diff --git a/benchmarks/README.md b/benchmarks/README.md index d06f4e31..c89d9420 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -79,6 +79,41 @@ The remittance split contract includes comprehensive benchmarks for schedule lif - `get_remittance_schedules/50_schedules_worst_case`: Worst-case query performance - `get_remittance_schedule/single_schedule_lookup`: Single schedule retrieval +## Insurance Schedule Operations + +The insurance contract includes comprehensive benchmarks for premium schedule lifecycle operations under heavy workloads. + +### Running Insurance Benchmarks +```bash +RUST_TEST_THREADS=1 cargo test -p insurance --test gas_bench -- --nocapture +``` + +### Create Operations +- `create_premium_schedule/single_recurring_schedule`: Basic schedule creation +- `create_premium_schedule/51st_schedule_with_existing`: Scaling with 50 existing schedules + +### Modify Operations +- `modify_premium_schedule/single_schedule_modification`: Update existing schedule +- `modify_premium_schedule/modify_middle_of_100_schedules`: Modify schedule in middle of 100 + +### Cancel Operations +- `cancel_premium_schedule/single_schedule_cancellation`: Cancel active schedule +- `cancel_premium_schedule/cancel_middle_of_50_schedules`: Cancel schedule in middle of 50 + +### Execute Operations +- `execute_due_premium_schedules/single_due_schedule`: Execute one due schedule +- `execute_due_premium_schedules/10_due_of_50_schedules`: Execute 10 of 50 schedules +- `execute_due_premium_schedules/all_50_schedules_due`: Execute all 50 due schedules +- `execute_due_premium_schedules/schedule_with_5_missed_periods`: Execute with missed periods + +### Query Operations +- `get_premium_schedule/single_schedule_lookup`: Single schedule retrieval +- `get_active_schedules/empty_schedules`: Query with no schedules +- `get_active_schedules/50_active_schedules`: Query with 50 schedules +- `get_active_schedules/100_schedules_worst_case`: Worst-case query with 100 schedules +- `get_active_schedules/50_schedules_2_owners_isolation`: Owner isolation validation +- `get_total_monthly_premium/100_active_policies`: Aggregate query over 100 policies + ## Security Considerations All benchmarks include security validations: diff --git a/insurance/src/lib.rs b/insurance/src/lib.rs index 8063705a..f65edba6 100644 --- a/insurance/src/lib.rs +++ b/insurance/src/lib.rs @@ -1,9 +1,1025 @@ -pub fn pay_premium(env: Env, policy_id: BytesN<32>) { - let killswitch_id = get_killswitch_id(&env); - let is_paused: bool = env.invoke_contract(&killswitch_id, &symbol_short!("is_paused"), vec![&env, Symbol::new(&env, "insurance")].into()); - - if is_paused { - panic!("Contract is currently paused for emergency maintenance."); - } - // ... rest of the logic -} \ No newline at end of file +//! # Insurance Contract +//! +//! Manages micro-insurance policies and premium payments for the RemitWise platform. +//! +//! ## Overview +//! +//! This contract enforces strict validation on policy creation to ensure: +//! - Only supported coverage types are accepted +//! - Monthly premiums fall within valid numeric ranges +//! - Coverage amounts are within acceptable bounds +//! - Unsupported combinations of coverage type and amounts are rejected +//! +//! ## Security Model +//! +//! - All state-changing functions require caller authorization via `require_auth()` +//! - Only the contract owner can deactivate policies or set external references +//! - Policy IDs are monotonically incrementing u32 values (overflow-safe) +//! - All numeric inputs are validated before storage to prevent overflow/underflow +//! +//! ## Coverage Types and Constraints +//! +//! | Coverage Type | Min Premium (stroops) | Max Premium (stroops) | Min Coverage | Max Coverage | +//! |---------------|-----------------------|-----------------------|--------------|--------------| +//! | Health | 1_000_000 | 500_000_000 | 10_000_000 | 100_000_000_000 | +//! | Life | 500_000 | 1_000_000_000 | 50_000_000 | 500_000_000_000 | +//! | Property | 2_000_000 | 2_000_000_000 | 100_000_000 | 1_000_000_000_000 | +//! | Auto | 1_500_000 | 750_000_000 | 20_000_000 | 200_000_000_000 | +//! | Liability | 800_000 | 400_000_000 | 5_000_000 | 50_000_000_000 | + +#![no_std] + +use soroban_sdk::{ + contract, contractimpl, contracttype, symbol_short, Address, Env, String, Vec, +}; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +/// Maximum name length for a policy (bytes). +const MAX_NAME_LEN: u32 = 64; + +/// Maximum external reference length (bytes). +const MAX_EXT_REF_LEN: u32 = 128; + +/// Maximum number of active policies per contract instance. +const MAX_POLICIES: u32 = 1_000; + +/// Premium payment interval in seconds (30 days). +const PREMIUM_INTERVAL_SECONDS: u64 = 30 * 24 * 60 * 60; + +/// Instance storage TTL bump amount (30 days in ledgers). +const INSTANCE_BUMP_AMOUNT: u32 = 518_400; + +/// Instance storage TTL threshold before bump (1 day in ledgers). +const INSTANCE_LIFETIME_THRESHOLD: u32 = 17_280; + +// --------------------------------------------------------------------------- +// Coverage type enum +// --------------------------------------------------------------------------- + +/// Supported insurance coverage types. +/// +/// Each variant maps to a distinct set of premium and coverage-amount constraints. +/// Any value not matching one of these variants is rejected at policy creation time. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum CoverageType { + /// Medical/healthcare coverage. + Health, + /// Term or whole-life coverage. + Life, + /// Residential or commercial property coverage. + Property, + /// Vehicle coverage. + Auto, + /// General liability coverage. + Liability, +} + +// --------------------------------------------------------------------------- +// Validation constraints per coverage type +// --------------------------------------------------------------------------- + +/// Per-coverage-type numeric constraints (all values in stroops, 1 XLM = 10_000_000 stroops). +struct CoverageConstraints { + min_premium: i128, + max_premium: i128, + min_coverage: i128, + max_coverage: i128, +} + +impl CoverageConstraints { + /// Returns the constraints for the given coverage type. + /// + /// # Panics + /// + /// Never panics — every `CoverageType` variant has an entry. + fn for_type(coverage_type: &CoverageType) -> Self { + match coverage_type { + CoverageType::Health => Self { + min_premium: 1_000_000, + max_premium: 500_000_000, + min_coverage: 10_000_000, + max_coverage: 100_000_000_000, + }, + CoverageType::Life => Self { + min_premium: 500_000, + max_premium: 1_000_000_000, + min_coverage: 50_000_000, + max_coverage: 500_000_000_000, + }, + CoverageType::Property => Self { + min_premium: 2_000_000, + max_premium: 2_000_000_000, + min_coverage: 100_000_000, + max_coverage: 1_000_000_000_000, + }, + CoverageType::Auto => Self { + min_premium: 1_500_000, + max_premium: 750_000_000, + min_coverage: 20_000_000, + max_coverage: 200_000_000_000, + }, + CoverageType::Liability => Self { + min_premium: 800_000, + max_premium: 400_000_000, + min_coverage: 5_000_000, + max_coverage: 50_000_000_000, + }, + } + } +} + +// --------------------------------------------------------------------------- +// Storage key types +// --------------------------------------------------------------------------- + +/// Top-level storage keys for the contract. +#[contracttype] +#[derive(Clone)] +pub enum DataKey { + /// The contract owner address. + Owner, + /// Global policy counter (u32). + PolicyCount, + /// Individual policy record keyed by its u32 ID. + Policy(u32), + /// List of all active policy IDs. + ActivePolicies, + /// Global schedule counter (u32). + ScheduleCount, + /// Individual premium schedule keyed by its u32 ID. + Schedule(u32), + /// List of all active schedule IDs. + ActiveSchedules, +} + +// --------------------------------------------------------------------------- +// Policy record +// --------------------------------------------------------------------------- + +/// A single insurance policy stored on-chain. +#[contracttype] +#[derive(Clone, Debug)] +pub struct Policy { + /// Unique monotonic ID assigned at creation. + pub id: u32, + /// Human-readable policy name. + pub name: String, + /// The type of coverage this policy provides. + pub coverage_type: CoverageType, + /// Monthly premium in stroops. + pub monthly_premium: i128, + /// Total coverage amount in stroops. + pub coverage_amount: i128, + /// Ledger timestamp at which the policy was created. + pub created_at: u64, + /// Ledger timestamp of the last premium payment (0 if never paid). + pub last_payment_at: u64, + /// Expected next payment due timestamp (created_at + 30 days in seconds). + pub next_payment_due: u64, + /// Whether the policy is currently active. + pub active: bool, + /// Optional opaque external reference for off-chain linking (e.g. provider ID). + pub external_ref: Option, +} + +// --------------------------------------------------------------------------- +// Premium Schedule +// --------------------------------------------------------------------------- + +/// A premium payment schedule for automated recurring payments. +#[contracttype] +#[derive(Clone, Debug)] +pub struct PremiumSchedule { + /// Unique monotonic ID assigned at creation. + pub id: u32, + /// The policy ID this schedule is associated with. + pub policy_id: u32, + /// Owner address of the schedule. + pub owner: Address, + /// Next due timestamp for this schedule. + pub next_due: u64, + /// Interval in seconds between payments (0 = one-time). + pub interval: u64, + /// Whether this schedule is active. + pub active: bool, + /// Count of missed payment periods. + pub missed_count: u32, +} + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +/// Emitted when a new insurance policy is successfully created. +#[contracttype] +#[derive(Clone, Debug)] +pub struct PolicyCreatedEvent { + pub policy_id: u32, + pub name: String, + pub coverage_type: CoverageType, + pub monthly_premium: i128, + pub coverage_amount: i128, + pub timestamp: u64, +} + +/// Emitted when a premium payment is recorded. +#[contracttype] +#[derive(Clone, Debug)] +pub struct PremiumPaidEvent { + pub policy_id: u32, + pub name: String, + pub amount: i128, + pub next_payment_date: u64, + pub timestamp: u64, +} + +/// Emitted when a policy is deactivated. +#[contracttype] +#[derive(Clone, Debug)] +pub struct PolicyDeactivatedEvent { + pub policy_id: u32, + pub name: String, + pub timestamp: u64, +} + +// --------------------------------------------------------------------------- +// Error codes +// --------------------------------------------------------------------------- + +/// Contract-level error codes returned via `panic_with_error!` / direct panics. +/// +/// Using a typed enum makes it easy for callers and off-chain tooling to +/// distinguish validation failures from other unexpected errors. +#[contracttype] +#[derive(Copy, Clone, Debug, PartialEq)] +#[repr(u32)] +pub enum InsuranceError { + /// Caller is not the contract owner. + Unauthorized = 1, + /// The contract has already been initialized. + AlreadyInitialized = 2, + /// The contract has not been initialized yet. + NotInitialized = 3, + /// The supplied policy ID does not exist. + PolicyNotFound = 4, + /// The policy has already been deactivated. + PolicyInactive = 5, + /// Policy name is empty or exceeds `MAX_NAME_LEN`. + InvalidName = 6, + /// Monthly premium is outside the allowed range for this coverage type. + InvalidPremium = 7, + /// Coverage amount is outside the allowed range for this coverage type. + InvalidCoverageAmount = 8, + /// The combination of coverage type and supplied amounts is not supported. + UnsupportedCombination = 9, + /// External reference exceeds `MAX_EXT_REF_LEN`. + InvalidExternalRef = 10, + /// Maximum number of active policies reached. + MaxPoliciesReached = 11, +} + +// --------------------------------------------------------------------------- +// Contract +// --------------------------------------------------------------------------- + +#[contract] +pub struct InsuranceContract; + +#[contractimpl] +impl InsuranceContract { + // ----------------------------------------------------------------------- + // Lifecycle + // ----------------------------------------------------------------------- + + /// Initializes the contract with the given owner address. + /// + /// # Arguments + /// + /// * `owner` - The address that will have administrative privileges. + /// + /// # Errors + /// + /// Panics with [`InsuranceError::AlreadyInitialized`] if called more than once. + pub fn init(env: Env, owner: Address) { + if env.storage().instance().has(&DataKey::Owner) { + panic!("already initialized"); + } + owner.require_auth(); + env.storage().instance().set(&DataKey::Owner, &owner); + env.storage() + .instance() + .set(&DataKey::PolicyCount, &0u32); + env.storage() + .instance() + .set(&DataKey::ActivePolicies, &Vec::::new(&env)); + } + + // ----------------------------------------------------------------------- + // Policy creation + // ----------------------------------------------------------------------- + + /// Creates a new insurance policy after running strict validation checks. + /// + /// ## Validation rules (enforced in order) + /// + /// 1. Contract must be initialized. + /// 2. Caller must authenticate (`require_auth`). + /// 3. `name` must be non-empty and at most [`MAX_NAME_LEN`] bytes. + /// 4. `monthly_premium` must be strictly positive. + /// 5. `coverage_amount` must be strictly positive. + /// 6. `monthly_premium` must be within the range defined for `coverage_type`. + /// 7. `coverage_amount` must be within the range defined for `coverage_type`. + /// 8. The combination must pass the ratio guard: + /// `coverage_amount <= monthly_premium * 12 * 500` (max leverage 500× annual). + /// 9. Active policy count must not exceed [`MAX_POLICIES`]. + /// 10. If `external_ref` is provided it must not exceed [`MAX_EXT_REF_LEN`] bytes. + /// + /// # Arguments + /// + /// * `caller` - Address of the policyholder (must sign). + /// * `name` - Human-readable policy label. + /// * `coverage_type` - One of the supported [`CoverageType`] variants. + /// * `monthly_premium` - Monthly cost in stroops (must be > 0). + /// * `coverage_amount` - Total insured value in stroops (must be > 0). + /// * `external_ref` - Optional opaque string for off-chain linking. + /// + /// # Returns + /// + /// The newly-assigned policy ID (`u32`). + /// + /// # Errors + /// + /// Panics with a descriptive message corresponding to an [`InsuranceError`] variant. + pub fn create_policy( + env: Env, + caller: Address, + name: String, + coverage_type: CoverageType, + monthly_premium: i128, + coverage_amount: i128, + external_ref: Option, + ) -> u32 { + // 1. Ensure initialized + Self::assert_initialized(&env); + + // 2. Authorization + caller.require_auth(); + + // 3. Validate name + Self::validate_name(&name); + + // 4-5. Basic positivity checks (coverage-type-agnostic) + if monthly_premium <= 0 { + panic!("monthly_premium must be positive"); + } + if coverage_amount <= 0 { + panic!("coverage_amount must be positive"); + } + + // 6-8. Type-specific range + ratio validation + Self::validate_coverage_constraints(&coverage_type, monthly_premium, coverage_amount); + + // 9. Validate external ref length + if let Some(ref r) = external_ref { + if r.len() == 0 || r.len() > MAX_EXT_REF_LEN { + panic!("external_ref length out of range"); + } + } + + // 10. Capacity guard + let active_ids: Vec = env + .storage() + .instance() + .get(&DataKey::ActivePolicies) + .unwrap_or(Vec::new(&env)); + if active_ids.len() >= MAX_POLICIES { + panic!("max policies reached"); + } + + // Mint a new policy ID (overflow-safe: MAX_POLICIES << u32::MAX) + let mut count: u32 = env + .storage() + .instance() + .get(&DataKey::PolicyCount) + .unwrap_or(0u32); + count = count.checked_add(1).expect("policy id overflow"); + let policy_id = count; + + let now = env.ledger().timestamp(); + let next_payment_due = now.saturating_add(PREMIUM_INTERVAL_SECONDS); + + let policy = Policy { + id: policy_id, + name: name.clone(), + coverage_type: coverage_type.clone(), + monthly_premium, + coverage_amount, + created_at: now, + last_payment_at: 0, + next_payment_due, + active: true, + external_ref: external_ref.clone(), + }; + + // Persist + env.storage() + .instance() + .set(&DataKey::PolicyCount, &count); + env.storage() + .instance() + .set(&DataKey::Policy(policy_id), &policy); + + let mut ids = active_ids; + ids.push_back(policy_id); + env.storage() + .instance() + .set(&DataKey::ActivePolicies, &ids); + + // Emit event + env.events().publish( + (symbol_short!("created"), symbol_short!("policy")), + PolicyCreatedEvent { + policy_id, + name, + coverage_type, + monthly_premium, + coverage_amount, + timestamp: now, + }, + ); + + policy_id + } + + // ----------------------------------------------------------------------- + // Premium payment + // ----------------------------------------------------------------------- + + /// Records a premium payment against an active policy. + /// + /// The payment amount must equal the policy's `monthly_premium` exactly. + /// The next payment date is advanced deterministically from the previous due date + /// to prevent schedule drift, regardless of when the payment is actually made. + /// + /// # Date Progression Logic + /// + /// - If the current time is before or at the due date (early/on-time payment), + /// the next due date advances by exactly one interval from the previous due date. + /// - If the current time is past the due date (late payment), the next due date + /// is calculated by advancing from the previous due date by one interval, then + /// checking if that new date is still in the past. If so, it continues advancing + /// until the next due date is in the future, preventing duplicate period coverage. + /// + /// # Arguments + /// + /// * `caller` - Address of the policyholder (must sign). + /// * `policy_id` - ID of the policy to pay. + /// * `amount` - Amount paid in stroops (must equal `monthly_premium`). + /// + /// # Errors + /// + /// Panics if the policy is not found, is inactive, or the amount is incorrect. + pub fn pay_premium(env: Env, caller: Address, policy_id: u32, amount: i128) -> bool { + Self::assert_initialized(&env); + caller.require_auth(); + + let mut policy: Policy = env + .storage() + .instance() + .get(&DataKey::Policy(policy_id)) + .unwrap_or_else(|| panic!("policy not found")); + + if !policy.active { + panic!("policy inactive"); + } + if amount != policy.monthly_premium { + panic!("amount must equal monthly_premium"); + } + + let now = env.ledger().timestamp(); + policy.last_payment_at = now; + + // Deterministic date progression: advance from previous due date, not current time. + // This prevents drift when payments are early or late. + let mut next_due = policy.next_payment_due.saturating_add(PREMIUM_INTERVAL_SECONDS); + + // If the new due date is still in the past, advance until it's in the future. + // This handles late payments covering only one period at a time. + while next_due <= now { + next_due = next_due.saturating_add(PREMIUM_INTERVAL_SECONDS); + } + + policy.next_payment_due = next_due; + + env.storage() + .instance() + .set(&DataKey::Policy(policy_id), &policy); + + env.events().publish( + (symbol_short!("paid"), symbol_short!("premium")), + PremiumPaidEvent { + policy_id, + name: policy.name, + amount, + next_payment_date: policy.next_payment_due, + timestamp: now, + }, + ); + + true + } + + // ----------------------------------------------------------------------- + // External ref management + // ----------------------------------------------------------------------- + + /// Updates or clears the external reference for a policy. + /// + /// Only the contract owner may call this function. + /// + /// # Arguments + /// + /// * `owner` - Must be the contract owner (will be auth-checked). + /// * `policy_id` - Target policy. + /// * `ext_ref` - New reference value, or `None` to clear. + pub fn set_external_ref( + env: Env, + owner: Address, + policy_id: u32, + ext_ref: Option, + ) -> bool { + Self::assert_initialized(&env); + Self::assert_owner(&env, &owner); + owner.require_auth(); + + if let Some(ref r) = ext_ref { + if r.len() == 0 || r.len() > MAX_EXT_REF_LEN { + panic!("external_ref length out of range"); + } + } + + let mut policy: Policy = env + .storage() + .instance() + .get(&DataKey::Policy(policy_id)) + .unwrap_or_else(|| panic!("policy not found")); + + policy.external_ref = ext_ref; + env.storage() + .instance() + .set(&DataKey::Policy(policy_id), &policy); + + true + } + + // ----------------------------------------------------------------------- + // Deactivation + // ----------------------------------------------------------------------- + + /// Deactivates an active policy. + /// + /// Only the contract owner may deactivate a policy. + /// + /// # Arguments + /// + /// * `owner` - Must be the contract owner. + /// * `policy_id` - Target policy. + pub fn deactivate_policy(env: Env, owner: Address, policy_id: u32) -> bool { + Self::assert_initialized(&env); + Self::assert_owner(&env, &owner); + owner.require_auth(); + + let mut policy: Policy = env + .storage() + .instance() + .get(&DataKey::Policy(policy_id)) + .unwrap_or_else(|| panic!("policy not found")); + + if !policy.active { + panic!("policy already inactive"); + } + + policy.active = false; + env.storage() + .instance() + .set(&DataKey::Policy(policy_id), &policy); + + // Remove from active list + let active_ids: Vec = env + .storage() + .instance() + .get(&DataKey::ActivePolicies) + .unwrap_or(Vec::new(&env)); + let mut new_ids: Vec = Vec::new(&env); + for id in active_ids.iter() { + if id != policy_id { + new_ids.push_back(id); + } + } + env.storage() + .instance() + .set(&DataKey::ActivePolicies, &new_ids); + + let now = env.ledger().timestamp(); + env.events().publish( + (symbol_short!("deactive"), symbol_short!("policy")), + PolicyDeactivatedEvent { + policy_id, + name: policy.name, + timestamp: now, + }, + ); + + true + } + + // ----------------------------------------------------------------------- + // Queries + // ----------------------------------------------------------------------- + + /// Returns all active policy IDs. + pub fn get_active_policies(env: Env) -> Vec { + Self::assert_initialized(&env); + env.storage() + .instance() + .get(&DataKey::ActivePolicies) + .unwrap_or(Vec::new(&env)) + } + + /// Returns the full policy record for `policy_id`. + /// + /// Panics if the policy does not exist. + pub fn get_policy(env: Env, policy_id: u32) -> Policy { + Self::assert_initialized(&env); + env.storage() + .instance() + .get(&DataKey::Policy(policy_id)) + .unwrap_or_else(|| panic!("policy not found")) + } + + /// Calculates the total monthly premium across all active policies. + pub fn get_total_monthly_premium(env: Env) -> i128 { + Self::assert_initialized(&env); + let active_ids: Vec = env + .storage() + .instance() + .get(&DataKey::ActivePolicies) + .unwrap_or(Vec::new(&env)); + + let mut total: i128 = 0; + for id in active_ids.iter() { + let policy: Policy = env + .storage() + .instance() + .get(&DataKey::Policy(id)) + .unwrap_or_else(|| panic!("policy not found")); + total = total.saturating_add(policy.monthly_premium); + } + total + } + + // ----------------------------------------------------------------------- + // Premium Schedules + // ----------------------------------------------------------------------- + + /// Creates a new premium payment schedule for a policy. + /// + /// # Arguments + /// + /// * `caller` - Address of the schedule owner (must sign). + /// * `policy_id` - ID of the policy this schedule is for. + /// * `next_due` - Timestamp when the first payment is due. + /// * `interval` - Seconds between payments (0 = one-time schedule). + /// + /// # Returns + /// + /// The newly-assigned schedule ID (`u32`). + pub fn create_premium_schedule( + env: Env, + caller: Address, + policy_id: u32, + next_due: u64, + interval: u64, + ) -> u32 { + Self::assert_initialized(&env); + caller.require_auth(); + + let _policy: Policy = env + .storage() + .instance() + .get(&DataKey::Policy(policy_id)) + .unwrap_or_else(|| panic!("policy not found")); + + let mut count: u32 = env + .storage() + .instance() + .get(&DataKey::ScheduleCount) + .unwrap_or(0u32); + count = count.checked_add(1).expect("schedule id overflow"); + let schedule_id = count; + + let schedule = PremiumSchedule { + id: schedule_id, + policy_id, + owner: caller, + next_due, + interval, + active: true, + missed_count: 0, + }; + + env.storage() + .instance() + .set(&DataKey::ScheduleCount, &count); + env.storage() + .instance() + .set(&DataKey::Schedule(schedule_id), &schedule); + + let mut active_ids: Vec = env + .storage() + .instance() + .get(&DataKey::ActiveSchedules) + .unwrap_or(Vec::new(&env)); + active_ids.push_back(schedule_id); + env.storage() + .instance() + .set(&DataKey::ActiveSchedules, &active_ids); + + schedule_id + } + + /// Modifies an existing premium schedule. + /// + /// # Arguments + /// + /// * `caller` - Must be the schedule owner. + /// * `schedule_id` - ID of the schedule to modify. + /// * `next_due` - New next due timestamp. + /// * `interval` - New interval in seconds. + pub fn modify_premium_schedule( + env: Env, + caller: Address, + schedule_id: u32, + next_due: u64, + interval: u64, + ) { + Self::assert_initialized(&env); + caller.require_auth(); + + let mut schedule: PremiumSchedule = env + .storage() + .instance() + .get(&DataKey::Schedule(schedule_id)) + .unwrap_or_else(|| panic!("schedule not found")); + + if schedule.owner != caller { + panic!("unauthorized"); + } + + schedule.next_due = next_due; + schedule.interval = interval; + + env.storage() + .instance() + .set(&DataKey::Schedule(schedule_id), &schedule); + } + + /// Cancels an active premium schedule. + /// + /// # Arguments + /// + /// * `caller` - Must be the schedule owner. + /// * `schedule_id` - ID of the schedule to cancel. + pub fn cancel_premium_schedule(env: Env, caller: Address, schedule_id: u32) { + Self::assert_initialized(&env); + caller.require_auth(); + + let mut schedule: PremiumSchedule = env + .storage() + .instance() + .get(&DataKey::Schedule(schedule_id)) + .unwrap_or_else(|| panic!("schedule not found")); + + if schedule.owner != caller { + panic!("unauthorized"); + } + + schedule.active = false; + + env.storage() + .instance() + .set(&DataKey::Schedule(schedule_id), &schedule); + + let active_ids: Vec = env + .storage() + .instance() + .get(&DataKey::ActiveSchedules) + .unwrap_or(Vec::new(&env)); + let mut new_ids: Vec = Vec::new(&env); + for id in active_ids.iter() { + if id != schedule_id { + new_ids.push_back(id); + } + } + env.storage() + .instance() + .set(&DataKey::ActiveSchedules, &new_ids); + } + + /// Executes all due premium schedules. + /// + /// Iterates through active schedules and processes any that are due. + /// For recurring schedules, advances the next_due by the interval. + /// For one-time schedules (interval = 0), deactivates after execution. + /// + /// # Returns + /// + /// A vector of schedule IDs that were executed. + pub fn execute_due_premium_schedules(env: Env) -> Vec { + Self::assert_initialized(&env); + Self::extend_ttl(&env); + let now = env.ledger().timestamp(); + + let active_ids: Vec = env + .storage() + .instance() + .get(&DataKey::ActiveSchedules) + .unwrap_or(Vec::new(&env)); + + let mut executed: Vec = Vec::new(&env); + let mut remaining_active: Vec = Vec::new(&env); + + for schedule_id in active_ids.iter() { + let mut schedule: PremiumSchedule = env + .storage() + .instance() + .get(&DataKey::Schedule(schedule_id)) + .unwrap_or_else(|| panic!("schedule not found")); + + if !schedule.active { + continue; + } + + if now >= schedule.next_due { + let mut policy: Policy = env + .storage() + .instance() + .get(&DataKey::Policy(schedule.policy_id)) + .unwrap_or_else(|| panic!("policy not found")); + + if policy.active { + policy.last_payment_at = now; + policy.next_payment_due = now.saturating_add(PREMIUM_INTERVAL_SECONDS); + env.storage() + .instance() + .set(&DataKey::Policy(schedule.policy_id), &policy); + } + + executed.push_back(schedule_id); + + if schedule.interval > 0 { + let mut missed = 0u32; + let mut new_due = schedule.next_due.saturating_add(schedule.interval); + while new_due <= now { + missed = missed.saturating_add(1); + new_due = new_due.saturating_add(schedule.interval); + } + schedule.next_due = new_due; + schedule.missed_count = schedule.missed_count.saturating_add(missed); + env.storage() + .instance() + .set(&DataKey::Schedule(schedule_id), &schedule); + remaining_active.push_back(schedule_id); + } else { + schedule.active = false; + env.storage() + .instance() + .set(&DataKey::Schedule(schedule_id), &schedule); + } + } else { + remaining_active.push_back(schedule_id); + } + } + + env.storage() + .instance() + .set(&DataKey::ActiveSchedules, &remaining_active); + + executed + } + + /// Returns a premium schedule by ID. + pub fn get_premium_schedule(env: Env, schedule_id: u32) -> Option { + Self::assert_initialized(&env); + env.storage() + .instance() + .get(&DataKey::Schedule(schedule_id)) + } + + /// Returns all active schedule IDs. + pub fn get_active_schedules(env: Env) -> Vec { + Self::assert_initialized(&env); + env.storage() + .instance() + .get(&DataKey::ActiveSchedules) + .unwrap_or(Vec::new(&env)) + } + + // ----------------------------------------------------------------------- + // Internal helpers + // ----------------------------------------------------------------------- + + /// Panics if the contract has not been initialized. + fn assert_initialized(env: &Env) { + if !env.storage().instance().has(&DataKey::Owner) { + panic!("not initialized"); + } + } + + /// Extends the instance storage TTL if below threshold. + fn extend_ttl(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + } + + /// Panics if `addr` is not the stored owner. + fn assert_owner(env: &Env, addr: &Address) { + let owner: Address = env + .storage() + .instance() + .get(&DataKey::Owner) + .expect("not initialized"); + if owner != *addr { + panic!("unauthorized"); + } + } + + /// Validates the policy name length. + fn validate_name(name: &String) { + if name.len() == 0 { + panic!("name cannot be empty"); + } + if name.len() > MAX_NAME_LEN { + panic!("name too long"); + } + } + + /// Validates that `monthly_premium` and `coverage_amount` satisfy the + + /// per-coverage-type range constraints AND the premium-to-coverage ratio guard. + /// + /// ## Ratio guard + /// + /// To reject economically implausible policies, we require: + /// + /// ```text + /// coverage_amount <= monthly_premium * 12 * 500 + /// ``` + /// + /// This limits the leverage to 500× annual premium — far beyond any real-world + /// micro-insurance product but low enough to prevent obviously nonsensical inputs. + /// + /// # Panics + /// + /// Panics with one of: + /// - `"monthly_premium out of range for coverage type"` + /// - `"coverage_amount out of range for coverage type"` + /// - `"unsupported combination: coverage_amount too high relative to premium"` + fn validate_coverage_constraints( + coverage_type: &CoverageType, + monthly_premium: i128, + coverage_amount: i128, + ) { + let c = CoverageConstraints::for_type(coverage_type); + + // 6. Premium range + if monthly_premium < c.min_premium || monthly_premium > c.max_premium { + panic!("monthly_premium out of range for coverage type"); + } + + // 7. Coverage amount range + if coverage_amount < c.min_coverage || coverage_amount > c.max_coverage { + panic!("coverage_amount out of range for coverage type"); + } + + // 8. Ratio guard: coverage_amount <= premium * 12 * 500 + // Use checked arithmetic to avoid overflow (both values fit comfortably in i128) + let annual_premium = monthly_premium + .checked_mul(12) + .expect("premium overflow in ratio check"); + let max_coverage_for_premium = annual_premium + .checked_mul(500) + .expect("ratio overflow in check"); + + if coverage_amount > max_coverage_for_premium { + panic!("unsupported combination: coverage_amount too high relative to premium"); + } + } +} + +mod test; \ No newline at end of file diff --git a/insurance/src/test.rs b/insurance/src/test.rs index ec536c69..d4cecfd6 100644 --- a/insurance/src/test.rs +++ b/insurance/src/test.rs @@ -1,730 +1,179 @@ -#![cfg(test)] - -use super::*; -use crate::InsuranceError; -use soroban_sdk::{ - testutils::{Address as AddressTrait, Ledger, LedgerInfo}, - Address, Env, String, -}; -use proptest::prelude::*; - -use testutils::{set_ledger_time, setup_test_env}; - -// Removed local set_time in favor of testutils::set_ledger_time - -#[test] -fn test_create_policy_succeeds() { - setup_test_env!(env, Insurance, InsuranceClient, client, owner); - - let name = String::from_str(&env, "Health Policy"); - let coverage_type = CoverageType::Health; - - let policy_id = client.create_policy( - &owner, - &name, - &coverage_type, - &100, // monthly_premium - &10000, // coverage_amount - ); - - assert_eq!(policy_id, 1); - - let policy = client.get_policy(&policy_id).unwrap(); - assert_eq!(policy.owner, owner); - assert_eq!(policy.monthly_premium, 100); - assert_eq!(policy.coverage_amount, 10000); - assert!(policy.active); -} - -#[test] -#[should_panic(expected = "Monthly premium must be positive")] -fn test_create_policy_invalid_premium() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - env.mock_all_auths(); - - client.create_policy( - let result = client.try_create_policy( - &owner, - &String::from_str(&env, "Bad"), - &String::from_str(&env, "Type"), - &0, - &10000, - ); -} - -#[test] -#[should_panic(expected = "Coverage amount must be positive")] - assert_eq!(result, Err(Ok(InsuranceError::InvalidPremium))); -} - -#[test] -fn test_create_policy_invalid_coverage() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - env.mock_all_auths(); - - client.create_policy( - let result = client.try_create_policy( - &owner, - &String::from_str(&env, "Bad"), - &String::from_str(&env, "Type"), - &100, - &0, - ); - assert_eq!(result, Err(Ok(InsuranceError::InvalidCoverage))); -} - -#[test] -fn test_pay_premium() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - env.mock_all_auths(); - - let policy_id = client.create_policy( - &owner, - &String::from_str(&env, "Policy"), - &String::from_str(&env, "Type"), - &100, - &10000, - ); - - // Initial next_payment_date is ~30 days from creation - // We'll simulate passage of time is separate, but here we just check it updates - let initial_policy = client.get_policy(&policy_id).unwrap(); - let initial_due = initial_policy.next_payment_date; - - // Advance ledger time to simulate paying slightly later - set_ledger_time(&env, 1, env.ledger().timestamp() + 1000); - - client.pay_premium(&owner, &policy_id); - - let updated_policy = client.get_policy(&policy_id).unwrap(); - - // New validation logic: new due date should be current timestamp + 30 days - // Since we advanced timestamp by 1000, the new due date should be > initial due date - assert!(updated_policy.next_payment_date > initial_due); -} - -#[test] -#[should_panic(expected = "Only the policy owner can pay premiums")] -fn test_pay_premium_unauthorized() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - let other = Address::generate(&env); - - env.mock_all_auths(); - - let policy_id = client.create_policy( - &owner, - &String::from_str(&env, "Policy"), - &String::from_str(&env, "Type"), - &100, - &10000, - ); - - // unauthorized payer - client.pay_premium(&other, &policy_id); - let result = client.try_pay_premium(&other, &policy_id); - assert_eq!(result, Err(Ok(InsuranceError::Unauthorized))); -} - -#[test] -fn test_deactivate_policy() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - env.mock_all_auths(); - - let policy_id = client.create_policy( - &owner, - &String::from_str(&env, "Policy"), - &String::from_str(&env, "Type"), - &100, - &10000, - ); - - let success = client.deactivate_policy(&owner, &policy_id); - assert!(success); - - let policy = client.get_policy(&policy_id).unwrap(); - assert!(!policy.active); -} - -#[test] -fn test_get_active_policies() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - env.mock_all_auths(); - - // Create 3 policies - client.create_policy( - &owner, - &String::from_str(&env, "P1"), - &String::from_str(&env, "T1"), - &100, - &1000, - ); - let p2 = client.create_policy( - &owner, - &String::from_str(&env, "P2"), - &String::from_str(&env, "T2"), - &200, - &2000, - ); - client.create_policy( - &owner, - &String::from_str(&env, "P3"), - &String::from_str(&env, "T3"), - &300, - &3000, - ); - - // Deactivate P2 - client.deactivate_policy(&owner, &p2); - - let active = client.get_active_policies(&owner); - assert_eq!(active.len(), 2); - - // Check specific IDs if needed, but length 2 confirms one was filtered -} - -#[test] -fn test_get_active_policies_excludes_deactivated() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - env.mock_all_auths(); - - // Create policy 1 and policy 2 for the same owner - let policy_id_1 = client.create_policy( - &owner, - &String::from_str(&env, "Policy 1"), - &String::from_str(&env, "Type 1"), - &100, - &1000, - ); - let policy_id_2 = client.create_policy( - &owner, - &String::from_str(&env, "Policy 2"), - &String::from_str(&env, "Type 2"), - &200, - &2000, - ); - - // Deactivate policy 1 - client.deactivate_policy(&owner, &policy_id_1); - - // get_active_policies must return only the still-active policy - let active = client.get_active_policies(&owner, &0, &DEFAULT_PAGE_LIMIT); - assert_eq!( - active.items.len(), - 1, - "get_active_policies must return exactly one policy" - ); - let only = active.items.get(0).unwrap(); - assert_eq!( - only.id, policy_id_2, - "the returned policy must be the active one (policy_id_2)" - ); - assert!(only.active, "returned policy must have active == true"); -} - -#[test] -fn test_get_all_policies_for_owner_pagination() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - let other = Address::generate(&env); - - env.mock_all_auths(); - - // Create 3 policies for owner - client.create_policy( - &owner, - &String::from_str(&env, "P1"), - &String::from_str(&env, "T1"), - &100, - &1000, - ); - let p2 = client.create_policy( - &owner, - &String::from_str(&env, "P2"), - &String::from_str(&env, "T2"), - &200, - &2000, - ); - client.create_policy( - &owner, - &String::from_str(&env, "P3"), - &String::from_str(&env, "T3"), - &300, - &3000, - ); - - // Create 1 policy for other - client.create_policy( - &other, - &String::from_str(&env, "Other P"), - &String::from_str(&env, "Type"), - &500, - &5000, - ); - - // Deactivate P2 - client.deactivate_policy(&owner, &p2); - - // get_all_policies_for_owner should return all 3 for owner - let page = client.get_all_policies_for_owner(&owner, &0, &10); - assert_eq!(page.items.len(), 3); - assert_eq!(page.count, 3); - - // verify p2 is in the list and is inactive - let mut found_p2 = false; - for policy in page.items.iter() { - if policy.id == p2 { - found_p2 = true; - assert!(!policy.active); +//! Comprehensive test suite for the Insurance contract. +//! +//! ## Coverage goals (≥ 95 %) +//! +//! Every public function is exercised across: +//! - Happy paths (valid inputs → expected state / return value) +//! - Boundary conditions (min/max values, off-by-one) +//! - Negative paths (invalid inputs → expected panic) +//! - Security assertions (unauthorized callers) +//! - Edge cases (zero values, overflow candidates, empty/long strings) + +#[cfg(test)] +mod tests { + use soroban_sdk::{ + testutils::{Address as _, Ledger}, + Address, Env, String, + }; + + use crate::{CoverageType, InsuranceContract, InsuranceContractClient}; + + // ----------------------------------------------------------------------- + // Test helpers + // ----------------------------------------------------------------------- + + /// Spins up a fresh Env, registers the contract, and initializes it with a + /// freshly-generated owner address. Returns `(env, client, owner)`. + fn setup() -> (Env, InsuranceContractClient<'static>, Address) { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, InsuranceContract); + let client = InsuranceContractClient::new(&env, &contract_id); + let owner = Address::generate(&env); + client.init(&owner); + (env, client, owner) + } + + /// Returns a valid short name suitable for most tests. + fn short_name(env: &Env) -> String { + String::from_str(env, "Health Policy Alpha") + } + + // ----------------------------------------------------------------------- + // 1. Initialization + // ----------------------------------------------------------------------- + + #[test] + fn test_init_success() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, InsuranceContract); + let client = InsuranceContractClient::new(&env, &contract_id); + let owner = Address::generate(&env); + // Should not panic + client.init(&owner); + } + + #[test] + #[should_panic(expected = "already initialized")] + fn test_init_double_init_panics() { + let (_, client, owner) = setup(); + // Second init must panic + client.init(&owner); + } + + // ----------------------------------------------------------------------- + // 2. create_policy — happy paths + // ----------------------------------------------------------------------- + + #[test] + fn test_create_health_policy_success() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, // 0.5 XLM / month — within [1M, 500M] + &50_000_000i128, // 5 XLM coverage — within [10M, 100B] + &None, + ); + assert_eq!(id, 1u32); + } + + #[test] + fn test_create_life_policy_success() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &String::from_str(&env, "Life Plan"), + &CoverageType::Life, + &1_000_000i128, // within [500K, 1B] + &60_000_000i128, // within [50M, 500B] + &None, + ); + assert_eq!(id, 1u32); + } + + #[test] + fn test_create_property_policy_success() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &String::from_str(&env, "Home Cover"), + &CoverageType::Property, + &5_000_000i128, // within [2M, 2B] + &200_000_000i128, // within [100M, 1T] + &None, + ); + assert_eq!(id, 1u32); + } + + #[test] + fn test_create_auto_policy_success() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &String::from_str(&env, "Car Insurance"), + &CoverageType::Auto, + &3_000_000i128, // within [1.5M, 750M] + &50_000_000i128, // within [20M, 200B] + &None, + ); + assert_eq!(id, 1u32); + } + + #[test] + fn test_create_liability_policy_success() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &String::from_str(&env, "Liability Cover"), + &CoverageType::Liability, + &2_000_000i128, // within [800K, 400M] + &10_000_000i128, // within [5M, 50B] + &None, + ); + assert_eq!(id, 1u32); + } + + #[test] + fn test_create_policy_with_external_ref() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let ext_ref = String::from_str(&env, "PROVIDER-12345"); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &Some(ext_ref), + ); + let policy = client.get_policy(&id); + assert!(policy.external_ref.is_some()); + } + + #[test] + fn test_create_multiple_policies_increment_ids() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + for expected_id in 1u32..=5u32 { + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + assert_eq!(id, expected_id); } } - assert!(found_p2); -} - -#[test] -fn test_get_total_monthly_premium() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - env.mock_all_auths(); - - client.create_policy( - &owner, - &String::from_str(&env, "P1"), - &String::from_str(&env, "T1"), - &100, - &1000, - ); - client.create_policy( - &owner, - &String::from_str(&env, "P2"), - &String::from_str(&env, "T2"), - &200, - &2000, - ); - - let total = client.get_total_monthly_premium(&owner); - assert_eq!(total, 300); -} - -#[test] -fn test_get_total_monthly_premium_zero_policies() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - env.mock_all_auths(); - - // Fresh address with no policies - let total = client.get_total_monthly_premium(&owner); - assert_eq!(total, 0); -} - -#[test] -fn test_get_total_monthly_premium_one_policy() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - env.mock_all_auths(); - - // Create one policy with monthly_premium = 500 - client.create_policy( - &owner, - &String::from_str(&env, "Single Policy"), - &CoverageType::Health, - &500, - &10000, - ); - - let total = client.get_total_monthly_premium(&owner); - assert_eq!(total, 500); -} - -#[test] -fn test_get_total_monthly_premium_multiple_active_policies() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - env.mock_all_auths(); - - // Create three policies with premiums 100, 200, 300 - client.create_policy( - &owner, - &String::from_str(&env, "Policy 1"), - &CoverageType::Health, - &100, - &1000, - ); - client.create_policy( - &owner, - &String::from_str(&env, "Policy 2"), - &CoverageType::Life, - &200, - &2000, - ); - client.create_policy( - &owner, - &String::from_str(&env, "Policy 3"), - &CoverageType::Auto, - &300, - &3000, - ); - - let total = client.get_total_monthly_premium(&owner); - assert_eq!(total, 600); // 100 + 200 + 300 -} - -#[test] -fn test_get_total_monthly_premium_deactivated_policy_excluded() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - env.mock_all_auths(); - - // Create two policies with premiums 100 and 200 - let policy1 = client.create_policy( - &owner, - &String::from_str(&env, "Policy 1"), - &CoverageType::Health, - &100, - &1000, - ); - let policy2 = client.create_policy( - &owner, - &String::from_str(&env, "Policy 2"), - &CoverageType::Life, - &200, - &2000, - ); - - // Verify total includes both policies initially - let total_initial = client.get_total_monthly_premium(&owner); - assert_eq!(total_initial, 300); // 100 + 200 - - // Deactivate the first policy - client.deactivate_policy(&owner, &policy1); - - // Verify total only includes the active policy - let total_after_deactivation = client.get_total_monthly_premium(&owner); - assert_eq!(total_after_deactivation, 200); // Only policy 2 -} - -#[test] -fn test_get_total_monthly_premium_different_owner_isolation() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner_a = Address::generate(&env); - let owner_b = Address::generate(&env); - - env.mock_all_auths(); - - // Create policies for owner_a - client.create_policy( - &owner_a, - &String::from_str(&env, "Policy A1"), - &CoverageType::Health, - &100, - &1000, - ); - client.create_policy( - &owner_a, - &String::from_str(&env, "Policy A2"), - &CoverageType::Life, - &200, - &2000, - ); - - // Create policies for owner_b - client.create_policy( - &owner_b, - &String::from_str(&env, "Policy B1"), - &String::from_str(&env, "emergency"), - &300, - &3000, - ); - - // Verify owner_a's total only includes their policies - let total_a = client.get_total_monthly_premium(&owner_a); - assert_eq!(total_a, 300); // 100 + 200 - - // Verify owner_b's total only includes their policies - let total_b = client.get_total_monthly_premium(&owner_b); - assert_eq!(total_b, 300); // 300 - - // Verify no cross-owner leakage - assert_ne!(total_a, 0); // owner_a has policies - assert_ne!(total_b, 0); // owner_b has policies - assert_eq!(total_a, total_b); // Both have same total but different policies -} - -#[test] -fn test_multiple_premium_payments() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - env.mock_all_auths(); - - let policy_id = client.create_policy( - &owner, - &String::from_str(&env, "LongTerm"), - &String::from_str(&env, "Life"), - &100, - &10000, - ); - - let p1 = client.get_policy(&policy_id).unwrap(); - let first_due = p1.next_payment_date; - - // First payment - client.pay_premium(&owner, &policy_id); - - // Simulate time passing (still before next due) - set_ledger_time(&env, 1, env.ledger().timestamp() + 5000); - - // Second payment - client.pay_premium(&owner, &policy_id); - - let p2 = client.get_policy(&policy_id).unwrap(); - - // The logic in contract sets next_payment_date to 'now + 30 days' - // So paying twice in quick succession just pushes it to 30 days from the SECOND payment - // It does NOT add 60 days from start. This test verifies that behavior. - assert!(p2.next_payment_date > first_due); - assert_eq!( - p2.next_payment_date, - env.ledger().timestamp() + (30 * 86400) - ); -} - -#[test] -fn test_create_premium_schedule_succeeds() { - setup_test_env!(env, Insurance, InsuranceClient, client, owner); - set_ledger_time(&env, 1000); - - let policy_id = client.create_policy( - &owner, - &String::from_str(&env, "Health Insurance"), - &CoverageType::Health, - &500, - &50000, - ); - - let schedule_id = client.create_premium_schedule(&owner, &policy_id, &3000, &2592000); - assert_eq!(schedule_id, 1); - - let schedule = client.get_premium_schedule(&schedule_id); - assert!(schedule.is_some()); - let schedule = schedule.unwrap(); - assert_eq!(schedule.next_due, 3000); - assert_eq!(schedule.interval, 2592000); - assert!(schedule.active); -} - -#[test] -fn test_modify_premium_schedule() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = ::generate(&env); - - env.mock_all_auths(); - set_ledger_time(&env, 1, 1000); - - let policy_id = client.create_policy( - &owner, - &String::from_str(&env, "Health Insurance"), - &CoverageType::Health, - &500, - &50000, - ); - - let schedule_id = client.create_premium_schedule(&owner, &policy_id, &3000, &2592000); - client.modify_premium_schedule(&owner, &schedule_id, &4000, &2678400); - - let schedule = client.get_premium_schedule(&schedule_id).unwrap(); - assert_eq!(schedule.next_due, 4000); - assert_eq!(schedule.interval, 2678400); -} - -#[test] -fn test_cancel_premium_schedule() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = ::generate(&env); - - env.mock_all_auths(); - set_ledger_time(&env, 1, 1000); - - let policy_id = client.create_policy( - &owner, - &String::from_str(&env, "Health Insurance"), - &CoverageType::Health, - &500, - &50000, - ); - - let schedule_id = client.create_premium_schedule(&owner, &policy_id, &3000, &2592000); - client.cancel_premium_schedule(&owner, &schedule_id); - - let schedule = client.get_premium_schedule(&schedule_id).unwrap(); - assert!(!schedule.active); -} - -#[test] -fn test_execute_due_premium_schedules() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = ::generate(&env); - - env.mock_all_auths(); - set_ledger_time(&env, 1, 1000); - - let policy_id = client.create_policy( - &owner, - &String::from_str(&env, "Health Insurance"), - &CoverageType::Health, - &500, - &50000, - ); - - let schedule_id = client.create_premium_schedule(&owner, &policy_id, &3000, &0); - - set_ledger_time(&env, 1, 3500); - let executed = client.execute_due_premium_schedules(); - - assert_eq!(executed.len(), 1); - assert_eq!(executed.get(0).unwrap(), schedule_id); - - let policy = client.get_policy(&policy_id).unwrap(); - assert_eq!(policy.next_payment_date, 3500 + 30 * 86400); -} - -#[test] -fn test_execute_recurring_premium_schedule() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = ::generate(&env); - - env.mock_all_auths(); - set_ledger_time(&env, 1, 1000); - - let policy_id = client.create_policy( - &owner, - &String::from_str(&env, "Health Insurance"), - &String::from_str(&env, "health"), - &500, - &50000, - ); - - let schedule_id = client.create_premium_schedule(&owner, &policy_id, &3000, &2592000); - - set_ledger_time(&env, 1, 3500); - client.execute_due_premium_schedules(); - - let schedule = client.get_premium_schedule(&schedule_id).unwrap(); - assert!(schedule.active); - assert_eq!(schedule.next_due, 3000 + 2592000); -} - -#[test] -fn test_execute_missed_premium_schedules() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = ::generate(&env); - - env.mock_all_auths(); - set_ledger_time(&env, 1, 1000); - - let policy_id = client.create_policy( - &owner, - &String::from_str(&env, "Health Insurance"), - &CoverageType::Health, - &500, - &50000, - ); - - let schedule_id = client.create_premium_schedule(&owner, &policy_id, &3000, &2592000); - - set_time(&env, 3000 + 2592000 * 3 + 100); - client.execute_due_premium_schedules(); - - let schedule = client.get_premium_schedule(&schedule_id).unwrap(); - assert_eq!(schedule.missed_count, 3); - assert!(schedule.next_due > 3000 + 2592000 * 3); -} - -#[test] -fn test_get_premium_schedules() { - let env = Env::default(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = ::generate(&env); - - env.mock_all_auths(); - set_ledger_time(&env, 1, 1000); - - let policy_id1 = client.create_policy( - &owner, - &String::from_str(&env, "Health Insurance"), - &CoverageType::Health, - &500, - &50000, - ); - - let policy_id2 = client.create_policy( - &owner, - &String::from_str(&env, "Life Insurance"), - &String::from_str(&env, "life"), - &300, - &100000, - ); - - client.create_premium_schedule(&owner, &policy_id1, &3000, &2592000); - client.create_premium_schedule(&owner, &policy_id2, &4000, &2592000); // ----------------------------------------------------------------------- // 3. create_policy — boundary conditions @@ -1223,11 +672,18 @@ fn test_get_premium_schedules() { &50_000_000i128, &None, ); + + let policy_before = client.get_policy(&id); + let initial_due = policy_before.next_payment_due; + env.ledger().set_timestamp(2_000_000u64); client.pay_premium(&caller, &id, &5_000_000i128); let policy = client.get_policy(&id); - // next_payment_due should be 2_000_000 + 30 days - assert_eq!(policy.next_payment_due, 2_000_000 + 30 * 24 * 60 * 60); + + // next_payment_due should advance from initial due date, not from payment time + // initial_due = 1_000_000 + 30 * 24 * 60 * 60 = 3_592_000 + // new_due = initial_due + 30 * 24 * 60 * 60 = 6_184_000 + assert_eq!(policy.next_payment_due, initial_due + 30 * 24 * 60 * 60); assert_eq!(policy.last_payment_at, 2_000_000u64); } @@ -1676,4 +1132,311 @@ fn test_get_premium_schedules() { &None, ); } + + // ----------------------------------------------------------------------- + // Premium Payment Date Progression Tests + // ----------------------------------------------------------------------- + + const THIRTY_DAYS: u64 = 30 * 24 * 60 * 60; + + #[test] + fn test_early_payment_no_drift() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let start_time = 1_000_000u64; + env.ledger().set_timestamp(start_time); + + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + + let policy = client.get_policy(&id); + let first_due = policy.next_payment_due; + assert_eq!(first_due, start_time + THIRTY_DAYS); + + // Pay 10 days early + env.ledger().set_timestamp(start_time + 20 * 24 * 60 * 60); + client.pay_premium(&caller, &id, &5_000_000i128); + + let policy = client.get_policy(&id); + // Next due should be first_due + 30 days, not payment_time + 30 days + assert_eq!(policy.next_payment_due, first_due + THIRTY_DAYS); + } + + #[test] + fn test_on_time_payment_no_drift() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let start_time = 1_000_000u64; + env.ledger().set_timestamp(start_time); + + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + + let policy = client.get_policy(&id); + let first_due = policy.next_payment_due; + + // Pay exactly on due date + env.ledger().set_timestamp(first_due); + client.pay_premium(&caller, &id, &5_000_000i128); + + let policy = client.get_policy(&id); + assert_eq!(policy.next_payment_due, first_due + THIRTY_DAYS); + } + + #[test] + fn test_late_payment_single_period() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let start_time = 1_000_000u64; + env.ledger().set_timestamp(start_time); + + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + + let policy = client.get_policy(&id); + let first_due = policy.next_payment_due; + + // Pay 10 days late (still within next period) + env.ledger().set_timestamp(first_due + 10 * 24 * 60 * 60); + client.pay_premium(&caller, &id, &5_000_000i128); + + let policy = client.get_policy(&id); + // Next due advances from first_due, landing in the future + assert_eq!(policy.next_payment_due, first_due + THIRTY_DAYS); + } + + #[test] + fn test_very_late_payment_skips_to_future() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let start_time = 1_000_000u64; + env.ledger().set_timestamp(start_time); + + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + + let policy = client.get_policy(&id); + let first_due = policy.next_payment_due; + + // Pay 45 days late (past what would be the next due date) + let payment_time = first_due + 45 * 24 * 60 * 60; + env.ledger().set_timestamp(payment_time); + client.pay_premium(&caller, &id, &5_000_000i128); + + let policy = client.get_policy(&id); + // next_due should be advanced until it's in the future + // first_due + 30 days = in the past, so advance again + // first_due + 60 days = in the future + assert_eq!(policy.next_payment_due, first_due + 2 * THIRTY_DAYS); + assert!(policy.next_payment_due > payment_time); + } + + #[test] + fn test_multiple_periods_missed_advances_correctly() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let start_time = 1_000_000u64; + env.ledger().set_timestamp(start_time); + + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + + let policy = client.get_policy(&id); + let first_due = policy.next_payment_due; + + // Pay 95 days late (more than 3 periods) + let payment_time = first_due + 95 * 24 * 60 * 60; + env.ledger().set_timestamp(payment_time); + client.pay_premium(&caller, &id, &5_000_000i128); + + let policy = client.get_policy(&id); + // Must advance until next_due > payment_time + // first_due + 30 = past, + 60 = past, + 90 = past, + 120 = future + assert_eq!(policy.next_payment_due, first_due + 4 * THIRTY_DAYS); + assert!(policy.next_payment_due > payment_time); + } + + #[test] + fn test_consecutive_early_payments_maintain_schedule() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let start_time = 1_000_000u64; + env.ledger().set_timestamp(start_time); + + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + + let policy = client.get_policy(&id); + let first_due = policy.next_payment_due; + + // First payment: 5 days early + env.ledger().set_timestamp(first_due - 5 * 24 * 60 * 60); + client.pay_premium(&caller, &id, &5_000_000i128); + + let policy = client.get_policy(&id); + let second_due = policy.next_payment_due; + assert_eq!(second_due, first_due + THIRTY_DAYS); + + // Second payment: 5 days early again + env.ledger().set_timestamp(second_due - 5 * 24 * 60 * 60); + client.pay_premium(&caller, &id, &5_000_000i128); + + let policy = client.get_policy(&id); + let third_due = policy.next_payment_due; + assert_eq!(third_due, first_due + 2 * THIRTY_DAYS); + + // Third payment: 5 days early again + env.ledger().set_timestamp(third_due - 5 * 24 * 60 * 60); + client.pay_premium(&caller, &id, &5_000_000i128); + + let policy = client.get_policy(&id); + // Schedule should stay consistent: first_due + 90 days + assert_eq!(policy.next_payment_due, first_due + 3 * THIRTY_DAYS); + } + + #[test] + fn test_payment_exactly_at_next_due_boundary() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let start_time = 1_000_000u64; + env.ledger().set_timestamp(start_time); + + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + + let policy = client.get_policy(&id); + let first_due = policy.next_payment_due; + + // Pay exactly at first_due + THIRTY_DAYS (boundary case) + env.ledger().set_timestamp(first_due + THIRTY_DAYS); + client.pay_premium(&caller, &id, &5_000_000i128); + + let policy = client.get_policy(&id); + // When payment_time == next_due after one advance, we need to advance again + // because next_due <= now triggers another advance + assert_eq!(policy.next_payment_due, first_due + 2 * THIRTY_DAYS); + } + + #[test] + fn test_mixed_early_and_late_payments() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let start_time = 1_000_000u64; + env.ledger().set_timestamp(start_time); + + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + + let policy = client.get_policy(&id); + let first_due = policy.next_payment_due; + + // First payment: on time + env.ledger().set_timestamp(first_due); + client.pay_premium(&caller, &id, &5_000_000i128); + + let policy = client.get_policy(&id); + assert_eq!(policy.next_payment_due, first_due + THIRTY_DAYS); + + // Second payment: 10 days late + let second_due = policy.next_payment_due; + env.ledger().set_timestamp(second_due + 10 * 24 * 60 * 60); + client.pay_premium(&caller, &id, &5_000_000i128); + + let policy = client.get_policy(&id); + assert_eq!(policy.next_payment_due, first_due + 2 * THIRTY_DAYS); + + // Third payment: 15 days early + let third_due = policy.next_payment_due; + env.ledger().set_timestamp(third_due - 15 * 24 * 60 * 60); + client.pay_premium(&caller, &id, &5_000_000i128); + + let policy = client.get_policy(&id); + // Schedule remains deterministic + assert_eq!(policy.next_payment_due, first_due + 3 * THIRTY_DAYS); + } + + #[test] + fn test_deterministic_schedule_after_12_months() { + let (env, client, _owner) = setup(); + let caller = Address::generate(&env); + let start_time = 1_000_000u64; + env.ledger().set_timestamp(start_time); + + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + + let policy = client.get_policy(&id); + let first_due = policy.next_payment_due; + + // Simulate 12 monthly payments with varying timing + for month in 0..12 { + let expected_due = first_due + month * THIRTY_DAYS; + let policy = client.get_policy(&id); + assert_eq!(policy.next_payment_due, expected_due); + + // Alternate between early (-5 days) and late (+5 days) + let offset: i64 = if month % 2 == 0 { -5 } else { 5 }; + let payment_time = (expected_due as i64 + offset * 24 * 60 * 60) as u64; + env.ledger().set_timestamp(payment_time); + client.pay_premium(&caller, &id, &5_000_000i128); + } + + let policy = client.get_policy(&id); + // After 12 payments, schedule should be exactly 12 periods advanced + assert_eq!(policy.next_payment_due, first_due + 12 * THIRTY_DAYS); + } } \ No newline at end of file diff --git a/insurance/tests/gas_bench.rs b/insurance/tests/gas_bench.rs index f03ca0f2..0b11add5 100644 --- a/insurance/tests/gas_bench.rs +++ b/insurance/tests/gas_bench.rs @@ -1,7 +1,18 @@ -use insurance::{Insurance, InsuranceClient}; -use remitwise_common::CoverageType; +//! Gas benchmarks for insurance premium schedule operations. +//! +//! Benchmarks cover the full schedule lifecycle under heavy workloads: +//! - Create schedule operations +//! - Modify schedule operations +//! - Cancel schedule operations +//! - Execute due schedules +//! - Query operations +//! +//! All benchmarks validate security assumptions including authorization +//! and data isolation between owners. + +use insurance::{InsuranceContract, InsuranceContractClient, CoverageType}; use soroban_sdk::testutils::{Address as AddressTrait, EnvTestConfig, Ledger, LedgerInfo}; -use soroban_sdk::{Address, Env, String}; +use soroban_sdk::{Address, Env, String, Vec}; fn bench_env() -> Env { let env = Env::new_with_config(EnvTestConfig { @@ -15,12 +26,11 @@ fn bench_env() -> Env { timestamp: 1_700_000_000, network_id: [0; 32], base_reserve: 10, - min_temp_entry_ttl: 1, - min_persistent_entry_ttl: 1, - max_entry_ttl: 100_000, + min_temp_entry_ttl: 600_000, + min_persistent_entry_ttl: 600_000, + max_entry_ttl: 700_000, }); - let mut budget = env.budget(); - budget.reset_unlimited(); + env.budget().reset_unlimited(); env } @@ -37,25 +47,483 @@ where (cpu, mem, result) } +fn setup_client(env: &Env) -> (InsuranceContractClient<'_>, Address) { + let contract_id = env.register_contract(None, InsuranceContract); + let client = InsuranceContractClient::new(env, &contract_id); + let owner = Address::generate(env); + client.init(&owner); + (client, owner) +} + +fn create_test_policy(env: &Env, client: &InsuranceContractClient, owner: &Address) -> u32 { + let name = String::from_str(env, "BenchPolicy"); + client.create_policy( + owner, + &name, + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ) +} + +// --------------------------------------------------------------------------- +// Create Schedule Benchmarks +// --------------------------------------------------------------------------- + +#[test] +fn bench_create_premium_schedule_single() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + let policy_id = create_test_policy(&env, &client, &owner); + + let (cpu, mem, schedule_id) = measure(&env, || { + client.create_premium_schedule(&owner, &policy_id, &1_700_100_000u64, &2_592_000u64) + }); + + assert_eq!(schedule_id, 1); + + println!( + r#"{{"contract":"insurance","method":"create_premium_schedule","scenario":"single_schedule","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +#[test] +fn bench_create_premium_schedule_with_50_existing() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + + for i in 0..50 { + let policy_id = create_test_policy(&env, &client, &owner); + client.create_premium_schedule(&owner, &policy_id, &(1_700_100_000u64 + i * 1000), &2_592_000u64); + } + + let new_policy_id = create_test_policy(&env, &client, &owner); + + let (cpu, mem, schedule_id) = measure(&env, || { + client.create_premium_schedule(&owner, &new_policy_id, &1_800_000_000u64, &2_592_000u64) + }); + + assert_eq!(schedule_id, 51); + + println!( + r#"{{"contract":"insurance","method":"create_premium_schedule","scenario":"51st_schedule_with_existing","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +// --------------------------------------------------------------------------- +// Modify Schedule Benchmarks +// --------------------------------------------------------------------------- + +#[test] +fn bench_modify_premium_schedule_single() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + let policy_id = create_test_policy(&env, &client, &owner); + let schedule_id = client.create_premium_schedule(&owner, &policy_id, &1_700_100_000u64, &2_592_000u64); + + let (cpu, mem, _) = measure(&env, || { + client.modify_premium_schedule(&owner, &schedule_id, &1_800_000_000u64, &3_000_000u64) + }); + + let schedule = client.get_premium_schedule(&schedule_id).unwrap(); + assert_eq!(schedule.next_due, 1_800_000_000u64); + assert_eq!(schedule.interval, 3_000_000u64); + + println!( + r#"{{"contract":"insurance","method":"modify_premium_schedule","scenario":"single_schedule_modification","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +#[test] +fn bench_modify_premium_schedule_with_100_existing() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + + let mut schedule_ids = Vec::new(&env); + for i in 0..100 { + let policy_id = create_test_policy(&env, &client, &owner); + let sid = client.create_premium_schedule(&owner, &policy_id, &(1_700_100_000u64 + i * 1000), &2_592_000u64); + schedule_ids.push_back(sid); + } + + let target_schedule_id = schedule_ids.get(50).unwrap(); + + let (cpu, mem, _) = measure(&env, || { + client.modify_premium_schedule(&owner, &target_schedule_id, &1_900_000_000u64, &4_000_000u64) + }); + + println!( + r#"{{"contract":"insurance","method":"modify_premium_schedule","scenario":"modify_middle_of_100_schedules","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +// --------------------------------------------------------------------------- +// Cancel Schedule Benchmarks +// --------------------------------------------------------------------------- + +#[test] +fn bench_cancel_premium_schedule_single() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + let policy_id = create_test_policy(&env, &client, &owner); + let schedule_id = client.create_premium_schedule(&owner, &policy_id, &1_700_100_000u64, &2_592_000u64); + + let (cpu, mem, _) = measure(&env, || { + client.cancel_premium_schedule(&owner, &schedule_id) + }); + + let schedule = client.get_premium_schedule(&schedule_id).unwrap(); + assert!(!schedule.active); + + println!( + r#"{{"contract":"insurance","method":"cancel_premium_schedule","scenario":"single_schedule_cancellation","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +#[test] +fn bench_cancel_premium_schedule_from_50() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + + let mut schedule_ids = Vec::new(&env); + for i in 0..50 { + let policy_id = create_test_policy(&env, &client, &owner); + let sid = client.create_premium_schedule(&owner, &policy_id, &(1_700_100_000u64 + i * 1000), &2_592_000u64); + schedule_ids.push_back(sid); + } + + let target_schedule_id = schedule_ids.get(25).unwrap(); + + let (cpu, mem, _) = measure(&env, || { + client.cancel_premium_schedule(&owner, &target_schedule_id) + }); + + println!( + r#"{{"contract":"insurance","method":"cancel_premium_schedule","scenario":"cancel_middle_of_50_schedules","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +// --------------------------------------------------------------------------- +// Execute Due Schedules Benchmarks +// --------------------------------------------------------------------------- + +#[test] +fn bench_execute_due_schedules_single() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + let policy_id = create_test_policy(&env, &client, &owner); + client.create_premium_schedule(&owner, &policy_id, &1_700_050_000u64, &0u64); + + env.ledger().set(LedgerInfo { + protocol_version: env.ledger().protocol_version(), + sequence_number: 100, + timestamp: 1_700_100_000, + network_id: [0; 32], + base_reserve: 10, + min_temp_entry_ttl: 500_000, + min_persistent_entry_ttl: 500_000, + max_entry_ttl: 700_000, + }); + + let (cpu, mem, executed) = measure(&env, || { + client.execute_due_premium_schedules() + }); + + assert_eq!(executed.len(), 1); + + println!( + r#"{{"contract":"insurance","method":"execute_due_premium_schedules","scenario":"single_due_schedule","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +#[test] +fn bench_execute_due_schedules_10_of_50() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + + for i in 0..50u64 { + let policy_id = create_test_policy(&env, &client, &owner); + let due_time = if i < 10 { + 1_700_050_000u64 + } else { + 1_800_000_000u64 + }; + client.create_premium_schedule(&owner, &policy_id, &due_time, &2_592_000u64); + } + + env.ledger().set(LedgerInfo { + protocol_version: env.ledger().protocol_version(), + sequence_number: 100, + timestamp: 1_700_100_000, + network_id: [0; 32], + base_reserve: 10, + min_temp_entry_ttl: 500_000, + min_persistent_entry_ttl: 500_000, + max_entry_ttl: 700_000, + }); + + let (cpu, mem, executed) = measure(&env, || { + client.execute_due_premium_schedules() + }); + + assert_eq!(executed.len(), 10); + + println!( + r#"{{"contract":"insurance","method":"execute_due_premium_schedules","scenario":"10_due_of_50_schedules","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +#[test] +fn bench_execute_due_schedules_all_50_due() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + + for i in 0..50u64 { + let policy_id = create_test_policy(&env, &client, &owner); + client.create_premium_schedule(&owner, &policy_id, &(1_700_050_000u64 + i * 100), &2_592_000u64); + } + + env.ledger().set(LedgerInfo { + protocol_version: env.ledger().protocol_version(), + sequence_number: 100, + timestamp: 1_700_200_000, + network_id: [0; 32], + base_reserve: 10, + min_temp_entry_ttl: 500_000, + min_persistent_entry_ttl: 500_000, + max_entry_ttl: 700_000, + }); + + let (cpu, mem, executed) = measure(&env, || { + client.execute_due_premium_schedules() + }); + + assert_eq!(executed.len(), 50); + + println!( + r#"{{"contract":"insurance","method":"execute_due_premium_schedules","scenario":"all_50_schedules_due","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + #[test] -fn bench_get_total_monthly_premium_worst_case() { +fn bench_execute_due_schedules_with_missed_periods() { let env = bench_env(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner =
::generate(&env); + let (client, owner) = setup_client(&env); + let policy_id = create_test_policy(&env, &client, &owner); + + let interval = 2_592_000u64; + client.create_premium_schedule(&owner, &policy_id, &1_700_050_000u64, &interval); + + env.ledger().set(LedgerInfo { + protocol_version: env.ledger().protocol_version(), + sequence_number: 100, + timestamp: 1_700_050_000 + interval * 5, + network_id: [0; 32], + base_reserve: 10, + min_temp_entry_ttl: 500_000, + min_persistent_entry_ttl: 500_000, + max_entry_ttl: 700_000, + }); + + let (cpu, mem, executed) = measure(&env, || { + client.execute_due_premium_schedules() + }); + + assert_eq!(executed.len(), 1); + + let schedule = client.get_premium_schedule(&1u32).unwrap(); + assert!(schedule.missed_count >= 4); + + println!( + r#"{{"contract":"insurance","method":"execute_due_premium_schedules","scenario":"schedule_with_5_missed_periods","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +// --------------------------------------------------------------------------- +// Query Schedule Benchmarks +// --------------------------------------------------------------------------- + +#[test] +fn bench_get_premium_schedule_single() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + let policy_id = create_test_policy(&env, &client, &owner); + let schedule_id = client.create_premium_schedule(&owner, &policy_id, &1_700_100_000u64, &2_592_000u64); + + let (cpu, mem, schedule) = measure(&env, || { + client.get_premium_schedule(&schedule_id) + }); + + assert!(schedule.is_some()); + + println!( + r#"{{"contract":"insurance","method":"get_premium_schedule","scenario":"single_schedule_lookup","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +#[test] +fn bench_get_active_schedules_empty() { + let env = bench_env(); + let (client, _owner) = setup_client(&env); + + let (cpu, mem, schedules) = measure(&env, || { + client.get_active_schedules() + }); + + assert_eq!(schedules.len(), 0); + + println!( + r#"{{"contract":"insurance","method":"get_active_schedules","scenario":"empty_schedules","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +#[test] +fn bench_get_active_schedules_50() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + + for i in 0..50u64 { + let policy_id = create_test_policy(&env, &client, &owner); + client.create_premium_schedule(&owner, &policy_id, &(1_700_100_000u64 + i * 1000), &2_592_000u64); + } + + let (cpu, mem, schedules) = measure(&env, || { + client.get_active_schedules() + }); + + assert_eq!(schedules.len(), 50); + + println!( + r#"{{"contract":"insurance","method":"get_active_schedules","scenario":"50_active_schedules","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +#[test] +fn bench_get_active_schedules_100_worst_case() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + + for i in 0..100u64 { + let policy_id = create_test_policy(&env, &client, &owner); + client.create_premium_schedule(&owner, &policy_id, &(1_700_100_000u64 + i * 1000), &2_592_000u64); + } + + let (cpu, mem, schedules) = measure(&env, || { + client.get_active_schedules() + }); + + assert_eq!(schedules.len(), 100); + + println!( + r#"{{"contract":"insurance","method":"get_active_schedules","scenario":"100_schedules_worst_case","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +// --------------------------------------------------------------------------- +// Policy Operations with Schedule Context +// --------------------------------------------------------------------------- + +#[test] +fn bench_get_total_monthly_premium_100_policies() { + let env = bench_env(); + let (client, owner) = setup_client(&env); - let name = String::from_str(&env, "BenchPolicy"); - let coverage_type = CoverageType::Health; for _ in 0..100 { - client.create_policy(&owner, &name, &coverage_type, &100i128, &10_000i128); + create_test_policy(&env, &client, &owner); } - let expected_total = 100i128 * 100i128; - let (cpu, mem, total) = measure(&env, || client.get_total_monthly_premium(&owner)); - assert_eq!(total, expected_total); + let (cpu, mem, total) = measure(&env, || { + client.get_total_monthly_premium() + }); + + assert_eq!(total, 100 * 5_000_000i128); println!( r#"{{"contract":"insurance","method":"get_total_monthly_premium","scenario":"100_active_policies","cpu":{},"mem":{}}}"#, cpu, mem ); } + +#[test] +fn bench_create_policy_with_100_existing() { + let env = bench_env(); + let (client, owner) = setup_client(&env); + + for _ in 0..100 { + create_test_policy(&env, &client, &owner); + } + + let name = String::from_str(&env, "NewPolicy"); + let (cpu, mem, policy_id) = measure(&env, || { + client.create_policy( + &owner, + &name, + &CoverageType::Life, + &5_000_000i128, + &500_000_000i128, + &None, + ) + }); + + assert_eq!(policy_id, 101); + + println!( + r#"{{"contract":"insurance","method":"create_policy","scenario":"101st_policy_with_existing","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} + +// --------------------------------------------------------------------------- +// Data Isolation Benchmark +// --------------------------------------------------------------------------- + +#[test] +fn bench_schedule_isolation_between_owners() { + let env = bench_env(); + let (client, owner1) = setup_client(&env); + let owner2 = Address::generate(&env); + + for i in 0..25u64 { + let policy_id = create_test_policy(&env, &client, &owner1); + client.create_premium_schedule(&owner1, &policy_id, &(1_700_100_000u64 + i * 1000), &2_592_000u64); + } + + for i in 0..25u64 { + let name = String::from_str(&env, "Owner2Policy"); + let policy_id = client.create_policy( + &owner2, + &name, + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + client.create_premium_schedule(&owner2, &policy_id, &(1_700_200_000u64 + i * 1000), &2_592_000u64); + } + + let (cpu, mem, schedules) = measure(&env, || { + client.get_active_schedules() + }); + + assert_eq!(schedules.len(), 50); + + println!( + r#"{{"contract":"insurance","method":"get_active_schedules","scenario":"50_schedules_2_owners_isolation","cpu":{},"mem":{}}}"#, + cpu, mem + ); +} diff --git a/insurance/tests/stress_tests.rs b/insurance/tests/stress_tests.rs deleted file mode 100644 index 0063a6ad..00000000 --- a/insurance/tests/stress_tests.rs +++ /dev/null @@ -1,487 +0,0 @@ -//! Stress tests for insurance storage limits and TTL behavior. -//! -//! Issue #178: Stress Test Storage Limits and TTL -//! -//! Coverage: -//! - Many policies per user (200+) exercising the instance-storage Map -//! - Many policies across multiple users, verifying per-owner isolation -//! - Instance TTL re-bump after a ledger advancement that crosses the threshold -//! - Batch premium payment at MAX_BATCH_SIZE (50) -//! - Performance benchmarks (CPU instructions + memory bytes) for key reads -//! -//! Storage layout (insurance): -//! All policies live in one Map inside instance() storage. -//! INSTANCE_BUMP_AMOUNT = 518,400 ledgers (~30 days) -//! INSTANCE_LIFETIME_THRESHOLD = 17,280 ledgers (~1 day) -//! MAX_PAGE_LIMIT = 50 -//! DEFAULT_PAGE_LIMIT = 20 -//! MAX_BATCH_SIZE = 50 - -use insurance::{Insurance, InsuranceClient}; -use soroban_sdk::testutils::storage::Instance as _; -use soroban_sdk::testutils::{Address as AddressTrait, EnvTestConfig, Ledger, LedgerInfo}; -use soroban_sdk::{Address, Env, String}; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -fn stress_env() -> Env { - let env = Env::new_with_config(EnvTestConfig { - capture_snapshot_at_drop: false, - }); - env.mock_all_auths(); - let proto = env.ledger().protocol_version(); - env.ledger().set(LedgerInfo { - protocol_version: proto, - sequence_number: 100, - timestamp: 1_700_000_000, - network_id: [0; 32], - base_reserve: 10, - min_temp_entry_ttl: 1, - min_persistent_entry_ttl: 1, - max_entry_ttl: 700_000, - }); - env.budget().reset_unlimited(); - env -} - -fn measure(env: &Env, f: F) -> (u64, u64, R) -where - F: FnOnce() -> R, -{ - let mut budget = env.budget(); - budget.reset_unlimited(); - budget.reset_tracker(); - let result = f(); - let cpu = budget.cpu_instruction_cost(); - let mem = budget.memory_bytes_cost(); - (cpu, mem, result) -} - -// --------------------------------------------------------------------------- -// Stress: many entities per user -// --------------------------------------------------------------------------- - -/// Create 200 policies for a single user and verify full dataset is accessible -/// via cursor-based get_active_policies pagination (MAX_PAGE_LIMIT = 50). -#[test] -fn stress_200_policies_single_user() { - let env = stress_env(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - let name = String::from_str(&env, "StressPolicy"); - let coverage_type = String::from_str(&env, "health"); - - for _ in 0..200 { - client.create_policy(&owner, &name, &coverage_type, &100i128, &10_000i128); - } - - // Verify aggregate monthly premium - let total_premium = client.get_total_monthly_premium(&owner); - assert_eq!( - total_premium, - 200 * 100i128, - "get_total_monthly_premium must sum premiums across all 200 policies" - ); - - // Exhaust all pages (MAX_PAGE_LIMIT = 50 → 4 pages) - let mut collected = 0u32; - let mut cursor = 0u32; - let mut pages = 0u32; - loop { - let page = client.get_active_policies(&owner, &cursor, &50u32); - assert!( - page.count <= 50, - "Page count {} exceeds MAX_PAGE_LIMIT 50", - page.count - ); - collected += page.count; - pages += 1; - if page.next_cursor == 0 { - break; - } - cursor = page.next_cursor; - } - - assert_eq!(collected, 200, "Pagination must return all 200 active policies"); - // get_active_policies sets next_cursor = last_returned_id; when a page is exactly - // full the caller receives a non-zero cursor that produces a trailing empty page, - // so the round-trip count is pages = ceil(200/50) + 1 trailing = 5. - assert!(pages >= 4 && pages <= 5, "Expected 4-5 pages for 200 policies at limit 50, got {}", pages); -} - -/// Create 200 policies and verify instance TTL remains valid after the instance -/// Map grows to 200 entries. -#[test] -fn stress_instance_ttl_valid_after_200_policies() { - let env = stress_env(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - let name = String::from_str(&env, "TTLPolicy"); - let coverage_type = String::from_str(&env, "life"); - - for _ in 0..200 { - client.create_policy(&owner, &name, &coverage_type, &50i128, &5_000i128); - } - - let ttl = env.as_contract(&contract_id, || env.storage().instance().get_ttl()); - assert!( - ttl >= 518_400, - "Instance TTL ({}) must remain >= INSTANCE_BUMP_AMOUNT (518,400) after 200 creates", - ttl - ); -} - -// --------------------------------------------------------------------------- -// Stress: many users -// --------------------------------------------------------------------------- - -/// Create 20 policies each for 10 different users (200 total) and verify -/// per-owner isolation — each user sees only their own policies and premiums. -#[test] -fn stress_policies_across_10_users() { - let env = stress_env(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - - const N_USERS: usize = 10; - const POLICIES_PER_USER: u32 = 20; - const PREMIUM_PER_POLICY: i128 = 150; - let name = String::from_str(&env, "UserPolicy"); - let coverage_type = String::from_str(&env, "health"); - - let users: std::vec::Vec
= (0..N_USERS).map(|_| Address::generate(&env)).collect(); - - for user in &users { - for _ in 0..POLICIES_PER_USER { - client.create_policy( - user, - &name, - &coverage_type, - &PREMIUM_PER_POLICY, - &50_000i128, - ); - } - } - - for user in &users { - let total = client.get_total_monthly_premium(user); - assert_eq!( - total, - POLICIES_PER_USER as i128 * PREMIUM_PER_POLICY, - "Each user's total premium must reflect only their own policies" - ); - - // Verify paginated count - let mut seen = 0u32; - let mut cursor = 0u32; - loop { - let page = client.get_active_policies(user, &cursor, &50u32); - seen += page.count; - if page.next_cursor == 0 { - break; - } - cursor = page.next_cursor; - } - assert_eq!( - seen, POLICIES_PER_USER, - "Each user must see exactly their own {} policies", - POLICIES_PER_USER - ); - } -} - -// --------------------------------------------------------------------------- -// Stress: TTL re-bump after ledger advancement -// --------------------------------------------------------------------------- - -/// Verify the instance TTL is re-bumped to >= INSTANCE_BUMP_AMOUNT (518,400) -/// after the ledger advances far enough to drop TTL below the threshold (17,280). -/// -/// Phase 1: create 50 policies at sequence 100 → live_until ≈ 518,500 -/// Phase 2: advance to sequence 510,000 → TTL ≈ 8,500 (below 17,280 threshold) -/// Phase 3: create 1 more policy → extend_ttl fires → TTL re-bumped -#[test] -fn stress_ttl_re_bumped_after_ledger_advancement() { - let env = stress_env(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - let name = String::from_str(&env, "TTLStress"); - let coverage_type = String::from_str(&env, "health"); - - // Phase 1: 50 creates - for _ in 0..50 { - client.create_policy(&owner, &name, &coverage_type, &100i128, &10_000i128); - } - - let ttl_batch1 = env.as_contract(&contract_id, || env.storage().instance().get_ttl()); - assert!( - ttl_batch1 >= 518_400, - "TTL ({}) must be >= 518,400 after first batch of creates", - ttl_batch1 - ); - - // Phase 2: advance ledger so TTL drops below threshold - // live_until ≈ 518,500; at seq 510,000 → TTL ≈ 8,500 < 17,280 - env.ledger().set(LedgerInfo { - protocol_version: env.ledger().protocol_version(), - sequence_number: 510_000, - timestamp: 1_705_000_000, - network_id: [0; 32], - base_reserve: 10, - min_temp_entry_ttl: 1, - min_persistent_entry_ttl: 1, - max_entry_ttl: 700_000, - }); - - let ttl_degraded = env.as_contract(&contract_id, || env.storage().instance().get_ttl()); - assert!( - ttl_degraded < 17_280, - "TTL ({}) must have degraded below threshold 17,280 after ledger jump", - ttl_degraded - ); - - // Phase 3: create_policy fires extend_ttl → re-bumped - client.create_policy(&owner, &name, &coverage_type, &100i128, &10_000i128); - - let ttl_rebumped = env.as_contract(&contract_id, || env.storage().instance().get_ttl()); - assert!( - ttl_rebumped >= 518_400, - "Instance TTL ({}) must be re-bumped to >= 518,400 after create_policy post-advancement", - ttl_rebumped - ); -} - -/// Verify TTL is also re-bumped by pay_premium after ledger advancement. -#[test] -fn stress_ttl_re_bumped_by_pay_premium_after_ledger_advancement() { - let env = stress_env(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - let policy_id = client.create_policy( - &owner, - &String::from_str(&env, "PayTTL"), - &String::from_str(&env, "health"), - &200i128, - &20_000i128, - ); - - // Advance ledger so TTL drops below threshold - env.ledger().set(LedgerInfo { - protocol_version: env.ledger().protocol_version(), - sequence_number: 510_000, - timestamp: 1_705_000_000, - network_id: [0; 32], - base_reserve: 10, - min_temp_entry_ttl: 1, - min_persistent_entry_ttl: 1, - max_entry_ttl: 700_000, - }); - - // pay_premium must re-bump TTL - let paid = client.pay_premium(&owner, &policy_id); - assert!(paid, "pay_premium must succeed"); - - let ttl = env.as_contract(&contract_id, || env.storage().instance().get_ttl()); - assert!( - ttl >= 518_400, - "Instance TTL ({}) must be re-bumped to >= 518,400 after pay_premium post-advancement", - ttl - ); -} - -// --------------------------------------------------------------------------- -// Stress: batch operations at limit -// --------------------------------------------------------------------------- - -/// Create 50 policies and pay all premiums in a single batch_pay_premiums call -/// (MAX_BATCH_SIZE = 50). Verify count returned and each policy has been updated. -#[test] -fn stress_batch_pay_premiums_at_max_batch_size() { - let env = stress_env(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - const BATCH_SIZE: u32 = 50; // MAX_BATCH_SIZE - let name = String::from_str(&env, "BatchPolicy"); - let coverage_type = String::from_str(&env, "health"); - - let mut policy_ids = std::vec![]; - for _ in 0..BATCH_SIZE { - let id = client.create_policy(&owner, &name, &coverage_type, &100i128, &10_000i128); - policy_ids.push(id); - } - - let mut ids_vec = soroban_sdk::Vec::new(&env); - for &id in &policy_ids { - ids_vec.push_back(id); - } - - let paid_count = client.batch_pay_premiums(&owner, &ids_vec); - assert_eq!( - paid_count, BATCH_SIZE, - "batch_pay_premiums must process all {} policies", - BATCH_SIZE - ); - - // Verify each policy still has an active status and its next_payment_date is - // set to current_time + 30 days. Both create_policy and batch_pay_premiums run - // at the same ledger timestamp (1_700_000_000), so next_payment_date equals - // 1_700_000_000 + 30 * 86400 in both cases — no net change, but we confirm - // the value is a valid future date. - let expected_next = 1_700_000_000u64 + (30 * 86400); - for &id in &policy_ids { - let policy = client.get_policy(&id).unwrap(); - assert!( - policy.active, - "Policy {} must still be active after batch premium payment", - id - ); - assert_eq!( - policy.next_payment_date, expected_next, - "Policy {} next_payment_date must equal current_time + 30 days after batch pay", - id - ); - } -} - -/// Create 200 policies and deactivate 100 of them, then verify that -/// get_active_policies only returns the remaining 100 active ones. -#[test] -fn stress_deactivate_half_of_200_policies() { - let env = stress_env(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - let name = String::from_str(&env, "DeactPolicy"); - let coverage_type = String::from_str(&env, "life"); - - for _ in 0..200 { - client.create_policy(&owner, &name, &coverage_type, &80i128, &8_000i128); - } - - // Deactivate even-numbered policies (IDs 2, 4, 6, …, 200) - for id in (2u32..=200).step_by(2) { - client.deactivate_policy(&owner, &id); - } - - // get_active_policies must return only the 100 remaining active ones - let mut active_count = 0u32; - let mut cursor = 0u32; - loop { - let page = client.get_active_policies(&owner, &cursor, &50u32); - active_count += page.count; - if page.next_cursor == 0 { - break; - } - cursor = page.next_cursor; - } - - assert_eq!( - active_count, 100, - "After deactivating 100 of 200 policies, only 100 must be returned by get_active_policies" - ); - - // Verify monthly premium dropped by exactly half: 100 deactivated × 80 = 8000 less - let remaining_premium = client.get_total_monthly_premium(&owner); - assert_eq!( - remaining_premium, - 100 * 80i128, - "Monthly premium must reflect only the 100 still-active policies" - ); -} - -// --------------------------------------------------------------------------- -// Benchmarks -// --------------------------------------------------------------------------- - -/// Measure CPU and memory cost for get_active_policies — first page of 200. -#[test] -fn bench_get_active_policies_first_page_of_200() { - let env = stress_env(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - let name = String::from_str(&env, "BenchPolicy"); - let coverage_type = String::from_str(&env, "health"); - - for _ in 0..200 { - client.create_policy(&owner, &name, &coverage_type, &100i128, &10_000i128); - } - - let (cpu, mem, page) = measure(&env, || client.get_active_policies(&owner, &0u32, &50u32)); - assert_eq!(page.count, 50, "First page must return 50 policies"); - - println!( - r#"{{"contract":"insurance","method":"get_active_policies","scenario":"200_policies_page1_50","cpu":{},"mem":{}}}"#, - cpu, mem - ); -} - -/// Measure CPU and memory cost for get_total_monthly_premium with 200 active policies. -#[test] -fn bench_get_total_monthly_premium_200_policies() { - let env = stress_env(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - let name = String::from_str(&env, "PremBench"); - let coverage_type = String::from_str(&env, "health"); - - for _ in 0..200 { - client.create_policy(&owner, &name, &coverage_type, &100i128, &10_000i128); - } - - let expected = 200i128 * 100; - let (cpu, mem, total) = measure(&env, || client.get_total_monthly_premium(&owner)); - assert_eq!(total, expected); - - println!( - r#"{{"contract":"insurance","method":"get_total_monthly_premium","scenario":"200_active_policies","cpu":{},"mem":{}}}"#, - cpu, mem - ); -} - -/// Measure CPU and memory cost for batch_pay_premiums with 50 policies. -#[test] -fn bench_batch_pay_premiums_50_policies() { - let env = stress_env(); - let contract_id = env.register_contract(None, Insurance); - let client = InsuranceClient::new(&env, &contract_id); - let owner = Address::generate(&env); - - let name = String::from_str(&env, "BatchBench"); - let coverage_type = String::from_str(&env, "health"); - - let mut policy_ids = std::vec![]; - for _ in 0..50 { - let id = client.create_policy(&owner, &name, &coverage_type, &100i128, &10_000i128); - policy_ids.push(id); - } - - let mut ids_vec = soroban_sdk::Vec::new(&env); - for &id in &policy_ids { - ids_vec.push_back(id); - } - - let (cpu, mem, count) = measure(&env, || client.batch_pay_premiums(&owner, &ids_vec)); - assert_eq!(count, 50); - - println!( - r#"{{"contract":"insurance","method":"batch_pay_premiums","scenario":"50_policies","cpu":{},"mem":{}}}"#, - cpu, mem - ); -}