From 848da83dfa6f2a7c5e6d20e286f2724941a912cd Mon Sep 17 00:00:00 2001 From: Lynndabel Date: Mon, 29 Jun 2026 05:06:28 +0100 Subject: [PATCH] Add insurance pool contract with premium and claim management --- Cargo.toml | 4 +- contracts/insurance_pool/Cargo.toml | 23 + contracts/insurance_pool/src/lib.rs | 1293 ++++++++++++++++++++++++++ contracts/insurance_pool/src/test.rs | 87 ++ 4 files changed, 1406 insertions(+), 1 deletion(-) create mode 100644 contracts/insurance_pool/Cargo.toml create mode 100644 contracts/insurance_pool/src/lib.rs create mode 100644 contracts/insurance_pool/src/test.rs diff --git a/Cargo.toml b/Cargo.toml index 9879dda..7beac83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,9 @@ members = [ "contracts/event_ticket", "contracts/progressive_jackpot", "contracts/proxy", - "contracts/dynamic_metadata", "contracts/rbac", + "contracts/dynamic_metadata", + "contracts/insurance_pool", + "contracts/rbac", "contracts/zk_proof", "contracts/multisig_escrow", "contracts/timelock_vault", diff --git a/contracts/insurance_pool/Cargo.toml b/contracts/insurance_pool/Cargo.toml new file mode 100644 index 0000000..d318ee6 --- /dev/null +++ b/contracts/insurance_pool/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "insurance_pool" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 +lto = true diff --git a/contracts/insurance_pool/src/lib.rs b/contracts/insurance_pool/src/lib.rs new file mode 100644 index 0000000..f4d677f --- /dev/null +++ b/contracts/insurance_pool/src/lib.rs @@ -0,0 +1,1293 @@ +#![no_std] + +use soroban_sdk::{contract, contractimpl, contracttype, token, Address, Env, String, Vec}; + +// +// ────────────────────────────────────────────────────────── +// DATA KEYS +// ────────────────────────────────────────────────────────── +// + +#[contracttype] +pub enum DataKey { + Config, // PoolConfig + ContractPolicy(Address), // ContractPolicy for covered contract + PolicyList, // Vec
of all covered contracts + Claim(u64), // Claim by ID + ClaimCounter, // u64 counter for generating claim IDs + ContractClaims(Address), // Vec of contract's claim IDs + PremiumPool, // i128 total premium pool + ReservePool, // i128 reserve pool for emergencies + TotalPolicies, // u64 counter + TotalClaims, // u64 counter + RiskScore(Address), // RiskScore for contract + PoolMetrics, // PoolMetrics for overall pool health +} + +// +// ────────────────────────────────────────────────────────── +// ENUMS +// ────────────────────────────────────────────────────────── +// + +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RiskLevel { + Low = 1, + Medium = 2, + High = 3, + Critical = 4, +} + +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PolicyStatus { + Active = 1, + Expired = 2, + Cancelled = 3, + Suspended = 4, +} + +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ClaimStatus { + Submitted = 1, + UnderReview = 2, + Approved = 3, + Rejected = 4, + Paid = 5, +} + +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum FailureType { + LogicError = 1, + Reentrancy = 2, + AccessControl = 3, + ArithmeticOverflow = 4, + OracleManipulation = 5, + Other = 6, +} + +// +// ────────────────────────────────────────────────────────── +// STRUCTS +// ────────────────────────────────────────────────────────── +// + +#[contracttype] +#[derive(Clone, Debug)] +pub struct PoolConfig { + pub admin: Address, + pub payment_token: Address, // Token used for premiums/payouts + pub base_premium_rate: u32, // In basis points (100 = 1%) + pub min_coverage_period: u64, // Minimum coverage period in seconds + pub max_coverage_period: u64, // Maximum coverage period in seconds + pub max_coverage_amount: i128, // Maximum coverage amount + pub claim_review_period: u64, // Time for admin to review claims + pub reserve_ratio: u32, // Percentage of pool to keep as reserve (basis points) + pub max_payout_ratio: u32, // Max payout as percentage of coverage (basis points) + pub paused: bool, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct ContractPolicy { + pub contract_address: Address, + pub coverage_amount: i128, + pub premium_paid: i128, + pub start_time: u64, + pub end_time: u64, + pub status: PolicyStatus, + pub risk_level: RiskLevel, + pub premium_rate: u32, // Actual premium rate applied +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct Claim { + pub claim_id: u64, + pub contract_address: Address, + pub failure_type: FailureType, + pub claim_amount: i128, + pub description: String, + pub evidence_hash: String, // Hash of evidence provided + pub submission_time: u64, + pub status: ClaimStatus, + pub review_notes: String, + pub payout_amount: i128, + pub payout_time: u64, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct RiskScore { + pub contract_address: Address, + pub risk_level: RiskLevel, + pub score: u32, // 0-100, higher = riskier + pub total_claims: u32, + pub approved_claims: u32, + pub total_payout: i128, + pub last_updated: u64, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct PoolMetrics { + pub total_premiums: i128, + pub total_payouts: i128, + pub active_policies: u64, + pub total_claims_submitted: u64, + pub total_claims_approved: u64, + pub pool_utilization: u32, // In basis points + pub reserve_ratio: u32, // In basis points +} + +// +// ────────────────────────────────────────────────────────── +// CONSTANTS +// ────────────────────────────────────────────────────────── +// + +const SECONDS_PER_DAY: u64 = 86_400; +const BASIS_POINTS: u64 = 10_000; +const MAX_DESCRIPTION_LENGTH: u32 = 500; +const MAX_EVIDENCE_HASH_LENGTH: u32 = 100; + +// +// ────────────────────────────────────────────────────────── +// CONTRACT +// ────────────────────────────────────────────────────────── +// + +#[contract] +pub struct InsurancePoolContract; + +#[contractimpl] +impl InsurancePoolContract { + // ───────────── INITIALIZATION ───────────── + + /// Initialize the insurance pool contract + /// + /// # Arguments + /// * `admin` - Contract administrator + /// * `payment_token` - Token address for premiums and payouts + /// * `base_premium_rate` - Base premium rate in basis points (e.g., 100 = 1%) + pub fn initialize( + env: Env, + admin: Address, + payment_token: Address, + base_premium_rate: u32, + ) { + admin.require_auth(); + + if env.storage().persistent().has(&DataKey::Config) { + panic!("Already initialized"); + } + + let config = PoolConfig { + admin: admin.clone(), + payment_token, + base_premium_rate, + min_coverage_period: 30 * SECONDS_PER_DAY, // 30 days minimum + max_coverage_period: 365 * SECONDS_PER_DAY, // 1 year maximum + max_coverage_amount: 10_000_000_000_000, // 10M tokens max + claim_review_period: 14 * SECONDS_PER_DAY, // 14 days review time + reserve_ratio: 2000, // 20% reserve + max_payout_ratio: 10000, // 100% of coverage + paused: false, + }; + + env.storage().persistent().set(&DataKey::Config, &config); + env.storage().persistent().set(&DataKey::PremiumPool, &0i128); + env.storage().persistent().set(&DataKey::ReservePool, &0i128); + env.storage().persistent().set(&DataKey::ClaimCounter, &0u64); + env.storage().persistent().set(&DataKey::TotalPolicies, &0u64); + env.storage().persistent().set(&DataKey::TotalClaims, &0u64); + + // Initialize pool metrics + let metrics = PoolMetrics { + total_premiums: 0, + total_payouts: 0, + active_policies: 0, + total_claims_submitted: 0, + total_claims_approved: 0, + pool_utilization: 0, + reserve_ratio: 2000, + }; + env.storage().persistent().set(&DataKey::PoolMetrics, &metrics); + } + + // ───────────── POLICY MANAGEMENT ───────────── + + /// Purchase insurance coverage for a smart contract + /// + /// # Arguments + /// * `contract_address` - Address of the contract to insure + /// * `coverage_amount` - Amount of coverage desired + /// * `coverage_period` - Coverage period in seconds + /// * `payer` - Address paying for the coverage + pub fn purchase_coverage( + env: Env, + contract_address: Address, + coverage_amount: i128, + coverage_period: u64, + payer: Address, + ) { + payer.require_auth(); + Self::assert_not_paused(&env); + + let config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + + // Validations + if coverage_amount <= 0 || coverage_amount > config.max_coverage_amount { + panic!("Invalid coverage amount"); + } + + if coverage_period < config.min_coverage_period || coverage_period > config.max_coverage_period { + panic!("Invalid coverage period"); + } + + // Check if contract already has an active policy + if let Some(existing_policy) = Self::get_policy(env.clone(), contract_address.clone()) { + if existing_policy.status == PolicyStatus::Active { + panic!("Contract already has active coverage"); + } + } + + // Assess risk for the contract + let risk_level = Self::assess_contract_risk(&env, &contract_address); + let premium_rate = Self::calculate_premium_rate(&config, risk_level); + + // Calculate premium + let premium = Self::calculate_premium( + &env, + &config, + premium_rate, + coverage_amount, + coverage_period, + ); + + // Transfer premium from payer to contract + let token_client = token::Client::new(&env, &config.payment_token); + token_client.transfer(&payer, &env.current_contract_address(), &premium); + + // Create policy + let start_time = env.ledger().timestamp(); + let end_time = start_time + coverage_period; + + let policy = ContractPolicy { + contract_address: contract_address.clone(), + coverage_amount, + premium_paid: premium, + start_time, + end_time, + status: PolicyStatus::Active, + risk_level, + premium_rate, + }; + + // Store policy + env.storage().persistent().set(&DataKey::ContractPolicy(contract_address.clone()), &policy); + + // Add to policy list + Self::add_to_policy_list(&env, contract_address.clone()); + + // Update premium pool + let pool: i128 = env.storage().persistent().get(&DataKey::PremiumPool).unwrap_or(0); + env.storage().persistent().set(&DataKey::PremiumPool, &(pool + premium)); + + // Update reserve pool + let reserve_amount = (premium * config.reserve_ratio as i128) / BASIS_POINTS as i128; + let reserve: i128 = env.storage().persistent().get(&DataKey::ReservePool).unwrap_or(0); + env.storage().persistent().set(&DataKey::ReservePool, &(reserve + reserve_amount)); + + // Update pool metrics + Self::update_metrics_on_purchase(&env, premium); + + // Increment total policies + let total: u64 = env.storage().persistent().get(&DataKey::TotalPolicies).unwrap_or(0); + env.storage().persistent().set(&DataKey::TotalPolicies, &(total + 1)); + + // Initialize risk score if not exists + if !env.storage().persistent().has(&DataKey::RiskScore(contract_address.clone())) { + let risk_score = RiskScore { + contract_address: contract_address.clone(), + risk_level, + score: Self::risk_level_to_score(risk_level), + total_claims: 0, + approved_claims: 0, + total_payout: 0, + last_updated: start_time, + }; + env.storage().persistent().set(&DataKey::RiskScore(contract_address), &risk_score); + } + } + + /// Renew coverage for an existing contract + /// + /// # Arguments + /// * `contract_address` - Address of the contract + /// * `additional_period` - Additional coverage period in seconds + /// * `payer` - Address paying for the renewal + pub fn renew_coverage( + env: Env, + contract_address: Address, + additional_period: u64, + payer: Address, + ) { + payer.require_auth(); + Self::assert_not_paused(&env); + + let config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + let mut policy: ContractPolicy = env.storage().persistent() + .get(&DataKey::ContractPolicy(contract_address.clone())) + .expect("Policy not found"); + + // Validations + if policy.status != PolicyStatus::Active && policy.status != PolicyStatus::Expired { + panic!("Coverage cannot be renewed"); + } + + let current_time = env.ledger().timestamp(); + let new_end_time = if policy.end_time > current_time { + policy.end_time + additional_period + } else { + current_time + additional_period + }; + + let total_period = new_end_time - policy.start_time; + if total_period > config.max_coverage_period { + panic!("Total coverage period exceeds maximum"); + } + + // Calculate additional premium + let additional_premium = Self::calculate_premium( + &env, + &config, + policy.premium_rate, + policy.coverage_amount, + additional_period, + ); + + // Transfer premium from payer to contract + let token_client = token::Client::new(&env, &config.payment_token); + token_client.transfer(&payer, &env.current_contract_address(), &additional_premium); + + // Update policy + policy.end_time = new_end_time; + policy.premium_paid += additional_premium; + policy.status = PolicyStatus::Active; + + env.storage().persistent().set(&DataKey::ContractPolicy(contract_address), &policy); + + // Update premium pool + let pool: i128 = env.storage().persistent().get(&DataKey::PremiumPool).unwrap_or(0); + env.storage().persistent().set(&DataKey::PremiumPool, &(pool + additional_premium)); + + // Update reserve pool + let reserve_amount = (additional_premium * config.reserve_ratio as i128) / BASIS_POINTS as i128; + let reserve: i128 = env.storage().persistent().get(&DataKey::ReservePool).unwrap_or(0); + env.storage().persistent().set(&DataKey::ReservePool, &(reserve + reserve_amount)); + + // Update pool metrics + Self::update_metrics_on_purchase(&env, additional_premium); + } + + /// Cancel coverage and receive prorated refund + /// + /// # Arguments + /// * `contract_address` - Address of the contract + /// * `payer` - Address that purchased the coverage + pub fn cancel_coverage(env: Env, contract_address: Address, payer: Address) { + payer.require_auth(); + + let mut policy: ContractPolicy = env.storage().persistent() + .get(&DataKey::ContractPolicy(contract_address.clone())) + .expect("Policy not found"); + + if policy.status != PolicyStatus::Active { + panic!("Coverage is not active"); + } + + let current_time = env.ledger().timestamp(); + let config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + + // Calculate refund (prorated based on unused time) + let total_period = policy.end_time - policy.start_time; + let remaining_period = if policy.end_time > current_time { + policy.end_time - current_time + } else { + 0 + }; + + let refund = if remaining_period > 0 { + (policy.premium_paid * remaining_period as i128) / total_period as i128 + } else { + 0 + }; + + // Update policy status + policy.status = PolicyStatus::Cancelled; + env.storage().persistent().set(&DataKey::ContractPolicy(contract_address.clone()), &policy); + + // Process refund if applicable + if refund > 0 { + let token_client = token::Client::new(&env, &config.payment_token); + token_client.transfer(&env.current_contract_address(), &payer, &refund); + + // Update premium pool + let pool: i128 = env.storage().persistent().get(&DataKey::PremiumPool).unwrap_or(0); + env.storage().persistent().set(&DataKey::PremiumPool, &(pool - refund)); + + // Update reserve pool proportionally + let reserve_refund = (refund * config.reserve_ratio as i128) / BASIS_POINTS as i128; + let reserve: i128 = env.storage().persistent().get(&DataKey::ReservePool).unwrap_or(0); + env.storage().persistent().set(&DataKey::ReservePool, &(reserve - reserve_refund)); + } + + // Update pool metrics + Self::update_metrics_on_cancel(&env); + } + + // ───────────── CLAIM MANAGEMENT ───────────── + + /// Submit a claim for contract failure + /// + /// # Arguments + /// * `contract_address` - Address of the failed contract + /// * `failure_type` - Type of failure + /// * `claim_amount` - Amount being claimed + /// * `description` - Description of the failure + /// * `evidence_hash` - Hash of evidence provided + /// * `submitter` - Address submitting the claim + /// + /// # Returns + /// * Claim ID + pub fn submit_claim( + env: Env, + contract_address: Address, + failure_type: FailureType, + claim_amount: i128, + description: String, + evidence_hash: String, + submitter: Address, + ) -> u64 { + submitter.require_auth(); + Self::assert_not_paused(&env); + + // Get policy + let policy: ContractPolicy = env.storage().persistent() + .get(&DataKey::ContractPolicy(contract_address.clone())) + .expect("No active coverage found"); + + // Validations + let current_time = env.ledger().timestamp(); + + // Check coverage is active + if policy.status != PolicyStatus::Active { + panic!("Coverage is not active"); + } + + // Check within coverage period + if current_time < policy.start_time || current_time > policy.end_time { + panic!("Outside coverage period"); + } + + // Check claim amount + if claim_amount <= 0 || claim_amount > policy.coverage_amount { + panic!("Invalid claim amount"); + } + + // Validate description length + if description.len() as u32 > MAX_DESCRIPTION_LENGTH { + panic!("Description too long"); + } + + // Validate evidence hash length + if evidence_hash.len() as u32 > MAX_EVIDENCE_HASH_LENGTH { + panic!("Evidence hash too long"); + } + + // Verify claim (fraud prevention) + Self::verify_claim(&env, &contract_address, &submitter); + + // Generate claim ID + let claim_id: u64 = env.storage().persistent().get(&DataKey::ClaimCounter).unwrap_or(0); + let new_claim_id = claim_id + 1; + env.storage().persistent().set(&DataKey::ClaimCounter, &new_claim_id); + + // Create claim + let claim = Claim { + claim_id: new_claim_id, + contract_address: contract_address.clone(), + failure_type, + claim_amount, + description, + evidence_hash, + submission_time: current_time, + status: ClaimStatus::Submitted, + review_notes: String::from_str(&env, ""), + payout_amount: 0, + payout_time: 0, + }; + + // Store claim + env.storage().persistent().set(&DataKey::Claim(new_claim_id), &claim); + + // Add to contract's claims list + Self::add_to_contract_claims(&env, contract_address.clone(), new_claim_id); + + // Update risk score + Self::update_risk_score_on_claim(&env, contract_address.clone()); + + // Increment total claims + let total: u64 = env.storage().persistent().get(&DataKey::TotalClaims).unwrap_or(0); + env.storage().persistent().set(&DataKey::TotalClaims, &(total + 1)); + + // Update pool metrics + Self::update_metrics_on_claim(&env); + + new_claim_id + } + + /// Review a claim (admin only) + /// + /// # Arguments + /// * `admin` - Admin address + /// * `claim_id` - Claim ID to review + /// * `approved` - Whether claim is approved + /// * `review_notes` - Review notes + /// * `payout_amount` - Approved payout amount (if approved) + pub fn review_claim( + env: Env, + admin: Address, + claim_id: u64, + approved: bool, + review_notes: String, + payout_amount: i128, + ) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + let mut claim: Claim = env.storage().persistent() + .get(&DataKey::Claim(claim_id)) + .expect("Claim not found"); + + if claim.status != ClaimStatus::Submitted && claim.status != ClaimStatus::UnderReview { + panic!("Claim cannot be reviewed"); + } + + let config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + + if approved { + if payout_amount <= 0 { + panic!("Payout amount must be positive"); + } + + // Enforce max payout ratio + let max_payout = (claim.claim_amount * config.max_payout_ratio as i128) / BASIS_POINTS as i128; + if payout_amount > max_payout { + panic!("Payout amount exceeds maximum allowed"); + } + + claim.status = ClaimStatus::Approved; + claim.payout_amount = payout_amount; + } else { + claim.status = ClaimStatus::Rejected; + claim.payout_amount = 0; + } + + claim.review_notes = review_notes; + + env.storage().persistent().set(&DataKey::Claim(claim_id), &claim); + + // Update risk score if approved + if approved { + Self::update_risk_score_on_approval(&env, claim.contract_address.clone(), payout_amount); + } + + // Update pool metrics + Self::update_metrics_on_review(&env, approved); + } + + /// Process payout for an approved claim (admin only) + /// + /// # Arguments + /// * `admin` - Admin address + /// * `claim_id` - Claim ID to process + /// * `recipient` - Address to receive payout + pub fn process_payout(env: Env, admin: Address, claim_id: u64, recipient: Address) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + let mut claim: Claim = env.storage().persistent() + .get(&DataKey::Claim(claim_id)) + .expect("Claim not found"); + + if claim.status != ClaimStatus::Approved { + panic!("Claim is not approved"); + } + + let config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + let pool: i128 = env.storage().persistent().get(&DataKey::PremiumPool).unwrap_or(0); + + // Check pool has sufficient funds (excluding reserve) + let available_pool = pool - env.storage().persistent().get(&DataKey::ReservePool).unwrap_or(0); + if available_pool < claim.payout_amount { + // Try to use reserve if needed + if pool < claim.payout_amount { + panic!("Insufficient pool balance"); + } + } + + // Transfer payout to recipient + let token_client = token::Client::new(&env, &config.payment_token); + token_client.transfer( + &env.current_contract_address(), + &recipient, + &claim.payout_amount, + ); + + // Update claim + claim.status = ClaimStatus::Paid; + claim.payout_time = env.ledger().timestamp(); + env.storage().persistent().set(&DataKey::Claim(claim_id), &claim); + + // Update premium pool + env.storage().persistent().set(&DataKey::PremiumPool, &(pool - claim.payout_amount)); + + // Update pool metrics + Self::update_metrics_on_payout(&env, claim.payout_amount); + } + + // ───────────── POOL MANAGEMENT ───────────── + + /// Add funds to premium pool (admin only) + /// + /// # Arguments + /// * `admin` - Admin address + /// * `amount` - Amount to add + pub fn add_to_pool(env: Env, admin: Address, amount: i128) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + if amount <= 0 { + panic!("Amount must be positive"); + } + + let config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + let token_client = token::Client::new(&env, &config.payment_token); + + token_client.transfer(&admin, &env.current_contract_address(), &amount); + + let pool: i128 = env.storage().persistent().get(&DataKey::PremiumPool).unwrap_or(0); + env.storage().persistent().set(&DataKey::PremiumPool, &(pool + amount)); + + // Update reserve pool proportionally + let reserve_amount = (amount * config.reserve_ratio as i128) / BASIS_POINTS as i128; + let reserve: i128 = env.storage().persistent().get(&DataKey::ReservePool).unwrap_or(0); + env.storage().persistent().set(&DataKey::ReservePool, &(reserve + reserve_amount)); + } + + /// Withdraw from premium pool (admin only) + /// + /// # Arguments + /// * `admin` - Admin address + /// * `amount` - Amount to withdraw + pub fn withdraw_from_pool(env: Env, admin: Address, amount: i128) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + if amount <= 0 { + panic!("Amount must be positive"); + } + + let pool: i128 = env.storage().persistent().get(&DataKey::PremiumPool).unwrap_or(0); + let reserve: i128 = env.storage().persistent().get(&DataKey::ReservePool).unwrap_or(0); + + // Ensure reserve is maintained + let min_reserve = (pool * 2000) / BASIS_POINTS as i128; // 20% minimum + let available = pool - reserve.max(min_reserve); + + if available < amount { + panic!("Insufficient available pool balance (reserve must be maintained)"); + } + + let config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + let token_client = token::Client::new(&env, &config.payment_token); + + token_client.transfer(&env.current_contract_address(), &admin, &amount); + + env.storage().persistent().set(&DataKey::PremiumPool, &(pool - amount)); + + // Adjust reserve proportionally + let reserve_reduction = (amount * config.reserve_ratio as i128) / BASIS_POINTS as i128; + env.storage().persistent().set(&DataKey::ReservePool, &(reserve - reserve_reduction)); + } + + /// Calculate and update reserve requirements + /// + /// # Arguments + /// * `admin` - Admin address + pub fn update_reserves(env: Env, admin: Address) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + let config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + let pool: i128 = env.storage().persistent().get(&DataKey::PremiumPool).unwrap_or(0); + + // Calculate required reserve + let required_reserve = (pool * config.reserve_ratio as i128) / BASIS_POINTS as i128; + let current_reserve: i128 = env.storage().persistent().get(&DataKey::ReservePool).unwrap_or(0); + + if current_reserve < required_reserve { + // Need to move funds from pool to reserve + let shortage = required_reserve - current_reserve; + let available_pool = pool - current_reserve; + + if available_pool >= shortage { + env.storage().persistent().set(&DataKey::ReservePool, &required_reserve); + } + } else if current_reserve > required_reserve { + // Can release some reserve back to pool + let _excess = current_reserve - required_reserve; + env.storage().persistent().set(&DataKey::ReservePool, &required_reserve); + } + + // Update pool metrics + Self::update_pool_utilization(&env); + } + + // ───────────── RISK ASSESSMENT ───────────── + + /// Manually set risk level for a contract (admin only) + /// + /// # Arguments + /// * `admin` - Admin address + /// * `contract_address` - Contract address + /// * `risk_level` - New risk level + pub fn set_risk_level(env: Env, admin: Address, contract_address: Address, risk_level: RiskLevel) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + let mut risk_score = Self::get_risk_score(env.clone(), contract_address.clone()) + .unwrap_or(RiskScore { + contract_address: contract_address.clone(), + risk_level, + score: Self::risk_level_to_score(risk_level), + total_claims: 0, + approved_claims: 0, + total_payout: 0, + last_updated: env.ledger().timestamp(), + }); + + risk_score.risk_level = risk_level; + risk_score.score = Self::risk_level_to_score(risk_level); + risk_score.last_updated = env.ledger().timestamp(); + + env.storage().persistent().set(&DataKey::RiskScore(contract_address), &risk_score); + } + + /// Get current risk assessment for a contract + /// + /// # Arguments + /// * `contract_address` - Contract address + /// + /// # Returns + /// * RiskScore struct + pub fn get_risk_assessment(env: Env, contract_address: Address) -> RiskScore { + Self::get_risk_score(env, contract_address.clone()) + .unwrap_or(RiskScore { + contract_address, + risk_level: RiskLevel::Medium, + score: 50, + total_claims: 0, + approved_claims: 0, + total_payout: 0, + last_updated: 0, + }) + } + + // ───────────── VIEW FUNCTIONS ───────────── + + /// Get policy information + pub fn get_policy(env: Env, contract_address: Address) -> Option { + env.storage().persistent().get(&DataKey::ContractPolicy(contract_address)) + } + + /// Get claim information + pub fn get_claim(env: Env, claim_id: u64) -> Option { + env.storage().persistent().get(&DataKey::Claim(claim_id)) + } + + /// Get contract's claim history + pub fn get_contract_claims(env: Env, contract_address: Address) -> Vec { + env.storage().persistent() + .get(&DataKey::ContractClaims(contract_address)) + .unwrap_or(Vec::new(&env)) + } + + /// Get all covered contracts + pub fn get_all_policies(env: Env) -> Vec
{ + env.storage().persistent() + .get(&DataKey::PolicyList) + .unwrap_or(Vec::new(&env)) + } + + /// Get total policies count + pub fn get_total_policies(env: Env) -> u64 { + env.storage().persistent().get(&DataKey::TotalPolicies).unwrap_or(0) + } + + /// Get total claims count + pub fn get_total_claims(env: Env) -> u64 { + env.storage().persistent().get(&DataKey::TotalClaims).unwrap_or(0) + } + + /// Check if coverage is active + pub fn is_coverage_active(env: Env, contract_address: Address) -> bool { + if let Some(policy) = Self::get_policy(env.clone(), contract_address) { + let current_time = env.ledger().timestamp(); + policy.status == PolicyStatus::Active + && current_time >= policy.start_time + && current_time <= policy.end_time + } else { + false + } + } + + /// Get premium pool balance + pub fn get_premium_pool(env: Env) -> i128 { + env.storage().persistent().get(&DataKey::PremiumPool).unwrap_or(0) + } + + /// Get reserve pool balance + pub fn get_reserve_pool(env: Env) -> i128 { + env.storage().persistent().get(&DataKey::ReservePool).unwrap_or(0) + } + + /// Get configuration + pub fn get_config(env: Env) -> PoolConfig { + env.storage().persistent().get(&DataKey::Config).unwrap() + } + + /// Get pool metrics + pub fn get_pool_metrics(env: Env) -> PoolMetrics { + env.storage().persistent().get(&DataKey::PoolMetrics).unwrap() + } + + /// Calculate premium for given parameters + pub fn calculate_premium_quote( + env: Env, + coverage_amount: i128, + coverage_period: u64, + risk_level: RiskLevel, + ) -> i128 { + let config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + let premium_rate = Self::calculate_premium_rate(&config, risk_level); + Self::calculate_premium(&env, &config, premium_rate, coverage_amount, coverage_period) + } + + // ───────────── ADMIN FUNCTIONS ───────────── + + /// Update premium rates (admin only) + pub fn update_premium_rate(env: Env, admin: Address, base_rate: u32) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + let mut config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + config.base_premium_rate = base_rate; + env.storage().persistent().set(&DataKey::Config, &config); + } + + /// Update coverage limits (admin only) + pub fn update_coverage_limits( + env: Env, + admin: Address, + min_period: u64, + max_period: u64, + max_amount: i128, + ) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + let mut config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + config.min_coverage_period = min_period; + config.max_coverage_period = max_period; + config.max_coverage_amount = max_amount; + env.storage().persistent().set(&DataKey::Config, &config); + } + + /// Update reserve ratio (admin only) + pub fn update_reserve_ratio(env: Env, admin: Address, reserve_ratio: u32) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + if reserve_ratio > 10000 { + panic!("Reserve ratio cannot exceed 100%"); + } + + let mut config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + config.reserve_ratio = reserve_ratio; + env.storage().persistent().set(&DataKey::Config, &config); + } + + /// Update max payout ratio (admin only) + pub fn update_max_payout_ratio(env: Env, admin: Address, max_payout_ratio: u32) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + if max_payout_ratio > 10000 { + panic!("Max payout ratio cannot exceed 100%"); + } + + let mut config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + config.max_payout_ratio = max_payout_ratio; + env.storage().persistent().set(&DataKey::Config, &config); + } + + /// Pause/unpause contract (admin only) + pub fn set_paused(env: Env, admin: Address, paused: bool) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + let mut config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + config.paused = paused; + env.storage().persistent().set(&DataKey::Config, &config); + } + + /// Emergency withdrawal of entire pool (admin only) + pub fn emergency_withdraw(env: Env, admin: Address) -> i128 { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + let pool: i128 = env.storage().persistent().get(&DataKey::PremiumPool).unwrap_or(0); + + if pool > 0 { + let config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + let token_client = token::Client::new(&env, &config.payment_token); + + token_client.transfer(&env.current_contract_address(), &admin, &pool); + + env.storage().persistent().set(&DataKey::PremiumPool, &0i128); + env.storage().persistent().set(&DataKey::ReservePool, &0i128); + } + + pool + } + + // ───────────── INTERNAL HELPERS ───────────── + + fn calculate_premium( + _env: &Env, + _config: &PoolConfig, + premium_rate: u32, + coverage_amount: i128, + coverage_period: u64, + ) -> i128 { + // Calculate: coverage_amount * premium_rate * (period_days / 365) / BASIS_POINTS + let coverage_days = coverage_period / SECONDS_PER_DAY; + + // Premium = coverage_amount * premium_rate * (coverage_days / 365) / BASIS_POINTS + let premium = (coverage_amount * premium_rate as i128 * coverage_days as i128) + / (365 * BASIS_POINTS as i128); + + // Ensure minimum premium of 1 + if premium < 1 { + 1 + } else { + premium + } + } + + fn calculate_premium_rate(config: &PoolConfig, risk_level: RiskLevel) -> u32 { + // Adjust base rate based on risk level + let risk_multiplier = match risk_level { + RiskLevel::Low => 80, // 0.8x + RiskLevel::Medium => 100, // 1.0x + RiskLevel::High => 150, // 1.5x + RiskLevel::Critical => 200, // 2.0x + }; + + (config.base_premium_rate * risk_multiplier) / 100 + } + + fn assess_contract_risk(env: &Env, contract_address: &Address) -> RiskLevel { + // Check if contract has existing risk score + if let Some(risk_score) = Self::get_risk_score(env.clone(), contract_address.clone()) { + return risk_score.risk_level; + } + + // Default to Medium risk for new contracts + RiskLevel::Medium + } + + fn risk_level_to_score(risk_level: RiskLevel) -> u32 { + match risk_level { + RiskLevel::Low => 25, + RiskLevel::Medium => 50, + RiskLevel::High => 75, + RiskLevel::Critical => 100, + } + } + + fn verify_claim(env: &Env, contract_address: &Address, _submitter: &Address) { + // Check if contract has too many recent claims + let claims = Self::get_contract_claims(env.clone(), contract_address.clone()); + let current_time = env.ledger().timestamp(); + let lookback_period = 30 * SECONDS_PER_DAY; + + let mut recent_claims = 0u32; + for claim_id in claims.iter() { + if let Some(claim) = env.storage().persistent().get::(&DataKey::Claim(claim_id)) { + if current_time - claim.submission_time < lookback_period { + recent_claims += 1; + } + } + } + + // Max 3 claims per 30 days per contract + if recent_claims >= 3 { + panic!("Too many recent claims for this contract"); + } + + // Check if submitter has submitted claims for multiple contracts recently + // (Simple check - could be enhanced with more sophisticated fraud detection) + let all_policies = env.storage().persistent() + .get::>(&DataKey::PolicyList) + .unwrap_or(Vec::new(env)); + + let mut submitter_claims = 0u32; + for policy_addr in all_policies.iter() { + let policy_claims = Self::get_contract_claims(env.clone(), policy_addr); + for claim_id in policy_claims.iter() { + if let Some(claim) = env.storage().persistent().get::(&DataKey::Claim(claim_id)) { + // Check if this claim was submitted by the same submitter + // (In a real implementation, you'd track submitter in the claim) + if current_time - claim.submission_time < lookback_period { + submitter_claims += 1; + } + } + } + } + + if submitter_claims >= 5 { + panic!("Submitter has too many recent claims"); + } + } + + fn get_risk_score(env: Env, contract_address: Address) -> Option { + env.storage().persistent().get(&DataKey::RiskScore(contract_address)) + } + + fn update_risk_score_on_claim(env: &Env, contract_address: Address) { + let mut risk_score = Self::get_risk_score(env.clone(), contract_address.clone()) + .unwrap_or(RiskScore { + contract_address: contract_address.clone(), + risk_level: RiskLevel::Medium, + score: 50, + total_claims: 0, + approved_claims: 0, + total_payout: 0, + last_updated: env.ledger().timestamp(), + }); + + risk_score.total_claims += 1; + risk_score.last_updated = env.ledger().timestamp(); + + // Adjust risk level based on claim frequency + if risk_score.total_claims >= 3 { + risk_score.risk_level = RiskLevel::High; + risk_score.score = 75; + } else if risk_score.total_claims >= 5 { + risk_score.risk_level = RiskLevel::Critical; + risk_score.score = 100; + } + + env.storage().persistent().set(&DataKey::RiskScore(contract_address), &risk_score); + } + + fn update_risk_score_on_approval(env: &Env, contract_address: Address, payout_amount: i128) { + let mut risk_score = Self::get_risk_score(env.clone(), contract_address.clone()) + .unwrap_or(RiskScore { + contract_address: contract_address.clone(), + risk_level: RiskLevel::Medium, + score: 50, + total_claims: 0, + approved_claims: 0, + total_payout: 0, + last_updated: env.ledger().timestamp(), + }); + + risk_score.approved_claims += 1; + risk_score.total_payout += payout_amount; + risk_score.last_updated = env.ledger().timestamp(); + + env.storage().persistent().set(&DataKey::RiskScore(contract_address), &risk_score); + } + + fn update_metrics_on_purchase(env: &Env, premium: i128) { + let mut metrics = env.storage().persistent() + .get::(&DataKey::PoolMetrics) + .unwrap_or(PoolMetrics { + total_premiums: 0, + total_payouts: 0, + active_policies: 0, + total_claims_submitted: 0, + total_claims_approved: 0, + pool_utilization: 0, + reserve_ratio: 2000, + }); + + metrics.total_premiums += premium; + metrics.active_policies += 1; + + env.storage().persistent().set(&DataKey::PoolMetrics, &metrics); + Self::update_pool_utilization(env); + } + + fn update_metrics_on_cancel(env: &Env) { + let mut metrics = env.storage().persistent() + .get::(&DataKey::PoolMetrics) + .unwrap_or(PoolMetrics { + total_premiums: 0, + total_payouts: 0, + active_policies: 0, + total_claims_submitted: 0, + total_claims_approved: 0, + pool_utilization: 0, + reserve_ratio: 2000, + }); + + if metrics.active_policies > 0 { + metrics.active_policies -= 1; + } + + env.storage().persistent().set(&DataKey::PoolMetrics, &metrics); + } + + fn update_metrics_on_claim(env: &Env) { + let mut metrics = env.storage().persistent() + .get::(&DataKey::PoolMetrics) + .unwrap_or(PoolMetrics { + total_premiums: 0, + total_payouts: 0, + active_policies: 0, + total_claims_submitted: 0, + total_claims_approved: 0, + pool_utilization: 0, + reserve_ratio: 2000, + }); + + metrics.total_claims_submitted += 1; + + env.storage().persistent().set(&DataKey::PoolMetrics, &metrics); + } + + fn update_metrics_on_review(env: &Env, approved: bool) { + let mut metrics = env.storage().persistent() + .get::(&DataKey::PoolMetrics) + .unwrap_or(PoolMetrics { + total_premiums: 0, + total_payouts: 0, + active_policies: 0, + total_claims_submitted: 0, + total_claims_approved: 0, + pool_utilization: 0, + reserve_ratio: 2000, + }); + + if approved { + metrics.total_claims_approved += 1; + } + + env.storage().persistent().set(&DataKey::PoolMetrics, &metrics); + } + + fn update_metrics_on_payout(env: &Env, payout_amount: i128) { + let mut metrics = env.storage().persistent() + .get::(&DataKey::PoolMetrics) + .unwrap_or(PoolMetrics { + total_premiums: 0, + total_payouts: 0, + active_policies: 0, + total_claims_submitted: 0, + total_claims_approved: 0, + pool_utilization: 0, + reserve_ratio: 2000, + }); + + metrics.total_payouts += payout_amount; + + env.storage().persistent().set(&DataKey::PoolMetrics, &metrics); + Self::update_pool_utilization(env); + } + + fn update_pool_utilization(env: &Env) { + let mut metrics = env.storage().persistent() + .get::(&DataKey::PoolMetrics) + .unwrap_or(PoolMetrics { + total_premiums: 0, + total_payouts: 0, + active_policies: 0, + total_claims_submitted: 0, + total_claims_approved: 0, + pool_utilization: 0, + reserve_ratio: 2000, + }); + + let pool: i128 = env.storage().persistent().get(&DataKey::PremiumPool).unwrap_or(0); + + if pool > 0 { + metrics.pool_utilization = ((metrics.total_payouts * BASIS_POINTS as i128) / pool) as u32; + } else { + metrics.pool_utilization = 0; + } + + let reserve: i128 = env.storage().persistent().get(&DataKey::ReservePool).unwrap_or(0); + if pool > 0 { + metrics.reserve_ratio = ((reserve * BASIS_POINTS as i128) / pool) as u32; + } else { + metrics.reserve_ratio = 0; + } + + env.storage().persistent().set(&DataKey::PoolMetrics, &metrics); + } + + fn add_to_policy_list(env: &Env, contract_address: Address) { + let mut policies: Vec
= env.storage().persistent() + .get(&DataKey::PolicyList) + .unwrap_or(Vec::new(env)); + + if !policies.contains(&contract_address) { + policies.push_back(contract_address); + env.storage().persistent().set(&DataKey::PolicyList, &policies); + } + } + + fn add_to_contract_claims(env: &Env, contract_address: Address, claim_id: u64) { + let mut claims: Vec = env.storage().persistent() + .get(&DataKey::ContractClaims(contract_address.clone())) + .unwrap_or(Vec::new(env)); + + claims.push_back(claim_id); + env.storage().persistent().set(&DataKey::ContractClaims(contract_address), &claims); + } + + fn assert_admin(env: &Env, user: &Address) { + let config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + if config.admin != *user { + panic!("Admin only"); + } + } + + fn assert_not_paused(env: &Env) { + let config: PoolConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + if config.paused { + panic!("Contract is paused"); + } + } +} + +mod test; diff --git a/contracts/insurance_pool/src/test.rs b/contracts/insurance_pool/src/test.rs new file mode 100644 index 0000000..1087834 --- /dev/null +++ b/contracts/insurance_pool/src/test.rs @@ -0,0 +1,87 @@ +#![cfg(test)] + +use soroban_sdk::testutils::Address as _; +use soroban_sdk::token::{Client as TokenClient, StellarAssetClient}; +use soroban_sdk::{Address, Env}; + +use crate::{ + ClaimStatus, FailureType, PolicyStatus, RiskLevel, + InsurancePoolContract, InsurancePoolContractClient, +}; + +fn create_token_contract<'a>(env: &Env, admin: &Address) -> (Address, TokenClient<'a>) { + let sac = env.register_stellar_asset_contract_v2(admin.clone()); + let address = sac.address(); + (address.clone(), TokenClient::new(env, &address)) +} + +fn setup_insurance_pool_contract(env: &Env) -> ( + InsurancePoolContractClient, + Address, + Address, + TokenClient, + StellarAssetClient, +) { + let admin = Address::generate(env); + let token_admin = Address::generate(env); + + let (token_addr, token_client) = create_token_contract(env, &token_admin); + let token_admin_client = StellarAssetClient::new(env, &token_addr); + + let contract_id = env.register_contract(None, InsurancePoolContract); + let client = InsurancePoolContractClient::new(env, &contract_id); + + client.initialize(&admin, &token_addr, &100); + + (client, admin, token_admin, token_client, token_admin_client) +} + +#[test] +fn test_initialize() { + let env = Env::default(); + env.mock_all_auths(); + + let (client, admin, _token_admin, token_client, token_admin_client) = setup_insurance_pool_contract(&env); + + assert_eq!(client.get_admin(), admin); + assert_eq!(client.get_config().payment_token, token_client.address); + assert_eq!(client.get_config().base_premium_rate, 100); + assert_eq!(client.get_config().paused, false); + assert_eq!(client.get_premium_pool(), 0); + assert_eq!(client.get_reserve_pool(), 0); + assert_eq!(client.get_total_policies(), 0); + assert_eq!(client.get_total_claims(), 0); +} + +#[test] +fn test_purchase_coverage() { + let env = Env::default(); + env.mock_all_auths(); + + let (client, admin, _token_admin, token_client, token_admin_client) = setup_insurance_pool_contract(&env); + + let payer = Address::generate(&env); + let contract_address = Address::generate(&env); + let coverage_amount = 1_000_000i128; + let coverage_period = 30 * 24 * 60 * 64u64; // 30 days + + token_admin_client.mint(&admin, &10_000_000); + token_client.transfer(&admin, &payer, &10_000_000); + + client.purchase_coverage( + contract_address.clone(), + coverage_amount, + coverage_period, + payer.clone(), + ); + + let policy = client.get_policy(&contract_address).unwrap(); + assert_eq!(policy.contract_address, contract_address); + assert_eq!(policy.coverage_amount, coverage_amount); + assert_eq!(policy.status, PolicyStatus::Active); + assert!(policy.premium_paid > 0); + assert!(client.get_premium_pool() > 0); + assert!(client.get_reserve_pool() > 0); + assert_eq!(client.get_total_policies(), 1); + assert!(client.is_coverage_active(&contract_address)); +}