diff --git a/audit.toml b/audit.toml new file mode 100644 index 00000000..c20a0e80 --- /dev/null +++ b/audit.toml @@ -0,0 +1,2 @@ +[advisories] +ignore = ["RUSTSEC-2026-0104", "RUSTSEC-2026-0002"] diff --git a/contracts/analytics/src/lib.rs b/contracts/analytics/src/lib.rs index 10f0398c..52caf0c9 100644 --- a/contracts/analytics/src/lib.rs +++ b/contracts/analytics/src/lib.rs @@ -67,6 +67,20 @@ mod propchain_analytics { pub volume_change_percentage: i32, } + /// Maximum number of items allowed in a batch operation to stay within gas limits. + const MAX_BATCH_SIZE: usize = 20; + + /// Data structure for a single metric update used in batch operations + #[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, + )] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + pub struct MetricUpdate { + pub average_price: u128, + pub total_volume: u128, + pub properties_listed: u64, + } + /// User behavior analytics for a specific account. #[derive( Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, @@ -137,6 +151,7 @@ mod propchain_analytics { NoPendingRotation, RotationUnauthorized, RequestExpired, + BatchSizeExceeded, } // ── Admin Key Rotation Events (Issue #496) ──────────────────────────────── @@ -164,6 +179,11 @@ mod propchain_analytics { old_admin: AccountId, cancelled_by: AccountId, } + #[ink(event)] + pub struct BatchMetricsUpdated { + #[ink(topic)] + count: u64, + } impl AnalyticsDashboard { #[ink(constructor)] @@ -211,6 +231,46 @@ mod propchain_analytics { }; } + /// Batch update multiple market metrics in a single transaction. + #[ink(message)] + pub fn batch_update_metrics( + &mut self, + updates: Vec, + ) -> Result<(), AnalyticsError> { + self.ensure_admin(); + if updates.len() > MAX_BATCH_SIZE { + return Err(AnalyticsError::BatchSizeExceeded); + } + for upd in updates.iter() { + self.current_metrics = MarketMetrics { + average_price: upd.average_price, + total_volume: upd.total_volume, + properties_listed: upd.properties_listed, + }; + } + self.env().emit_event(BatchMetricsUpdated { + count: updates.len() as u64, + }); + Ok(()) + } + + /// Batch add multiple market trends in a single transaction. + #[ink(message)] + pub fn batch_add_trends(&mut self, trends: Vec) -> Result<(), AnalyticsError> { + self.ensure_admin(); + if trends.len() > MAX_BATCH_SIZE { + return Err(AnalyticsError::BatchSizeExceeded); + } + for trend in trends.iter() { + self.historical_trends.insert(self.trend_count, trend); + self.trend_count += 1; + } + self.env().emit_event(BatchMetricsUpdated { + count: trends.len() as u64, + }); + Ok(()) + } + /// Create market trend analysis with historical data #[ink(message)] pub fn add_market_trend(&mut self, trend: MarketTrend) { @@ -770,6 +830,78 @@ mod propchain_analytics { < residential_first.target_allocation_bips ); } + + #[ink::test] + fn batch_update_metrics_success() { + let mut contract = AnalyticsDashboard::new(); + let mut updates = Vec::new(); + for i in 0..20u64 { + updates.push(MetricUpdate { + average_price: (i * 10) as u128, + total_volume: (i * 100) as u128, + properties_listed: i as u64, + }); + } + assert_eq!(contract.batch_update_metrics(updates.clone()), Ok(())); + let metrics = contract.get_market_metrics(); + let last = updates.last().unwrap(); + assert_eq!(metrics.average_price, last.average_price); + assert_eq!(metrics.total_volume, last.total_volume); + assert_eq!(metrics.properties_listed, last.properties_listed); + } + + #[ink::test] + fn batch_update_metrics_exceeds_limit() { + let mut contract = AnalyticsDashboard::new(); + let mut updates = Vec::new(); + for i in 0..21u64 { + updates.push(MetricUpdate { + average_price: i as u128, + total_volume: i as u128, + properties_listed: i as u64, + }); + } + assert_eq!( + contract.batch_update_metrics(updates), + Err(AnalyticsError::BatchSizeExceeded) + ); + } + + #[ink::test] + fn batch_add_trends_success() { + let mut contract = AnalyticsDashboard::new(); + let mut trends = Vec::new(); + for i in 0..20u64 { + trends.push(MarketTrend { + period_start: i * 10, + period_end: i * 10 + 5, + price_change_percentage: (i % 5) as i32, + volume_change_percentage: (i % 7) as i32, + }); + } + assert_eq!(contract.batch_add_trends(trends.clone()), Ok(())); + let stored = contract.get_historical_trends(); + assert_eq!(stored.len(), 20); + assert_eq!(stored[0].period_start, trends[0].period_start); + } + + #[ink::test] + fn batch_add_trends_exceeds_limit() { + let mut contract = AnalyticsDashboard::new(); + let mut trends = Vec::new(); + for i in 0..21u64 { + trends.push(MarketTrend { + period_start: i, + period_end: i + 1, + price_change_percentage: 0, + volume_change_percentage: 0, + }); + } + assert_eq!( + contract.batch_add_trends(trends), + Err(AnalyticsError::BatchSizeExceeded) + ); + } } } diff --git a/contracts/insurance/src/lib.rs b/contracts/insurance/src/lib.rs index 48da6ddd..c3e66b64 100644 --- a/contracts/insurance/src/lib.rs +++ b/contracts/insurance/src/lib.rs @@ -15,6 +15,9 @@ mod risk_assessment; // Fraud Detection System (Task #258) mod fraud_detection; +// Premium calculation engine +mod premium_engine; + /// Decentralized Property Insurance Platform #[ink::contract] mod propchain_insurance { @@ -1174,10 +1177,7 @@ mod propchain_insurance { /// Deactivate an active trigger. Only the policyholder or admin may /// deactivate. Already-fired triggers cannot be re-deactivated. #[ink(message)] - pub fn deactivate_claim_trigger( - &mut self, - trigger_id: u64, - ) -> Result<(), InsuranceError> { + pub fn deactivate_claim_trigger(&mut self, trigger_id: u64) -> Result<(), InsuranceError> { let caller = self.env().caller(); let mut trigger = self .claim_triggers @@ -1280,8 +1280,7 @@ mod propchain_insurance { if remaining == 0 { return Err(InsuranceError::ClaimExceedsCoverage); } - let claim_amount = - Self::compute_claim_amount(&trigger.payout_mode, remaining)?; + let claim_amount = Self::compute_claim_amount(&trigger.payout_mode, remaining)?; if claim_amount == 0 { return Err(InsuranceError::TriggerConditionNotMet); } @@ -1310,8 +1309,10 @@ mod propchain_insurance { policy.claims_count += 1; self.policies.insert(&trigger.policy_id, &policy); - let mut policy_claims = - self.policy_claims.get(&trigger.policy_id).unwrap_or_default(); + let mut policy_claims = self + .policy_claims + .get(&trigger.policy_id) + .unwrap_or_default(); policy_claims.push(claim_id); self.policy_claims .insert(&trigger.policy_id, &policy_claims); @@ -1911,7 +1912,8 @@ mod propchain_insurance { .get(&caller) .unwrap_or_default(); holder_list.push(policy_id); - self.holder_parametric_policies.insert(&caller, &holder_list); + self.holder_parametric_policies + .insert(&caller, &holder_list); self.env().emit_event(ParametricPolicyCreated { policy_id, @@ -2243,8 +2245,8 @@ mod propchain_insurance { } let block = self.env().block_number(); - let effective_at = block - .saturating_add(propchain_traits::constants::KEY_ROTATION_COOLDOWN_BLOCKS); + let effective_at = + block.saturating_add(propchain_traits::constants::KEY_ROTATION_COOLDOWN_BLOCKS); self.pending_admin_rotation = Some(propchain_traits::KeyRotationRequest { old_account: caller, @@ -2330,9 +2332,7 @@ mod propchain_insurance { /// Get the pending admin rotation request, if any. #[ink(message)] - pub fn get_pending_admin_rotation( - &self, - ) -> Option { + pub fn get_pending_admin_rotation(&self) -> Option { self.pending_admin_rotation.clone() } @@ -2940,11 +2940,14 @@ mod propchain_insurance { } /// Find a suitable pool for the given coverage type - fn find_pool_for_coverage(&self, coverage_type: &CoverageType) -> Result { + fn find_pool_for_coverage( + &self, + coverage_type: &CoverageType, + ) -> Result { // Iterate through pools to find an active one matching the coverage type // For now, return the first active pool or a default let pool_count = self.pool_count; - + for pool_id in 1..=pool_count { if let Some(pool) = self.pools.get(&pool_id) { if pool.is_active && pool.coverage_type == *coverage_type { @@ -2966,10 +2969,13 @@ mod propchain_insurance { } /// Get actuarial model for coverage type - fn get_actuarial_model_for_coverage(&self, coverage_type: &CoverageType) -> Option { + fn get_actuarial_model_for_coverage( + &self, + coverage_type: &CoverageType, + ) -> Option { // Search through models to find one matching the coverage type let model_count = self.model_count; - + for model_id in 1..=model_count { if let Some(model) = self.actuarial_models.get(&model_id) { if model.coverage_type == *coverage_type { @@ -2977,7 +2983,7 @@ mod propchain_insurance { } } } - + None } @@ -3035,7 +3041,9 @@ mod propchain_insurance { self.circuit_breaker_active = true; let now = self.env().block_timestamp(); // Determine pool_id for the event - let pool_id = self.policies.get(&policy_id) + let pool_id = self + .policies + .get(&policy_id) .map(|p| p.pool_id) .unwrap_or(0); self.env().emit_event(CircuitBreakerTripped { diff --git a/contracts/insurance/src/premium_engine.rs b/contracts/insurance/src/premium_engine.rs index e4eec07a..74fc422d 100644 --- a/contracts/insurance/src/premium_engine.rs +++ b/contracts/insurance/src/premium_engine.rs @@ -64,9 +64,8 @@ pub fn calculate_dynamic_premium( / PREMIUM_CALCULATION_DIVISOR; // Prorate for policy duration - let duration_premium = annual_premium - .saturating_mul(policy_duration_seconds as u128) - / SECONDS_PER_YEAR; + let duration_premium = + annual_premium.saturating_mul(policy_duration_seconds as u128) / SECONDS_PER_YEAR; let monthly_premium = duration_premium / 12; @@ -85,7 +84,8 @@ pub fn calculate_dynamic_premium( monthly_premium, deductible, breakdown: PremiumBreakdown { - base_premium: coverage_amount.saturating_mul(base_rate as u128) / BASIS_POINTS_DENOMINATOR, + base_premium: coverage_amount.saturating_mul(base_rate as u128) + / BASIS_POINTS_DENOMINATOR, risk_adjustment: calculate_risk_adjustment_amount( coverage_amount, base_rate, @@ -136,13 +136,16 @@ pub fn calculate_dynamic_premium( } /// Calculate base rate from actuarial model -fn calculate_base_rate(actuarial_model: Option<&ActuarialModel>, coverage_type: &CoverageType) -> u32 { +fn calculate_base_rate( + actuarial_model: Option<&ActuarialModel>, + coverage_type: &CoverageType, +) -> u32 { match actuarial_model { Some(model) => { // Use actuarial model: expected_loss_ratio * confidence_adjustment // Expected loss ratio in basis points (e.g., 600 = 6%) let expected_loss = model.expected_loss_ratio; - + // Confidence level adjustment (95% = 1.0, 99% = 1.2) let confidence_adjustment = match model.confidence_level { 95 => 100, @@ -155,7 +158,7 @@ fn calculate_base_rate(actuarial_model: Option<&ActuarialModel>, coverage_type: // Base rate = expected_loss * confidence_adjustment / 100 let model_rate = expected_loss.saturating_mul(confidence_adjustment as u32) / 100; - + // Add expense loading (20% for operational costs) model_rate.saturating_mul(120) / 100 } @@ -169,13 +172,13 @@ fn calculate_base_rate(actuarial_model: Option<&ActuarialModel>, coverage_type: /// Default base rates by coverage type fn coverage_type_base_rate(coverage_type: &CoverageType) -> u32 { match coverage_type { - CoverageType::Fire => 120, // 1.2% - CoverageType::Flood => 200, // 2.0% - CoverageType::Earthquake => 250, // 2.5% - CoverageType::Theft => 100, // 1.0% + CoverageType::Fire => 120, // 1.2% + CoverageType::Flood => 200, // 2.0% + CoverageType::Earthquake => 250, // 2.5% + CoverageType::Theft => 100, // 1.0% CoverageType::LiabilityDamage => 150, // 1.5% CoverageType::NaturalDisaster => 220, // 2.2% - CoverageType::Comprehensive => 300, // 3.0% + CoverageType::Comprehensive => 300, // 3.0% } } @@ -194,16 +197,16 @@ fn calculate_risk_multiplier(assessment: &RiskAssessment) -> u32 { // Convert score (0-100) to multiplier (50-400 basis points) // Score 0 = very high risk (4.0x), Score 100 = very low risk (0.5x) match weighted_score { - 0..=10 => 400, // Very high risk - 11..=20 => 350, // High risk - 21..=30 => 300, // High-medium risk - 31..=40 => 250, // Medium-high risk - 41..=50 => 200, // Medium risk - 51..=60 => 170, // Medium-low risk - 61..=70 => 140, // Low-medium risk - 71..=80 => 110, // Low risk - 81..=90 => 85, // Very low risk - _ => 60, // Minimal risk + 0..=10 => 400, // Very high risk + 11..=20 => 350, // High risk + 21..=30 => 300, // High-medium risk + 31..=40 => 250, // Medium-high risk + 41..=50 => 200, // Medium risk + 51..=60 => 170, // Medium-low risk + 61..=70 => 140, // Low-medium risk + 71..=80 => 110, // Low risk + 81..=90 => 85, // Very low risk + _ => 60, // Minimal risk } } @@ -233,11 +236,11 @@ fn calculate_pool_utilization_multiplier(pool: &RiskPool) -> u32 { // Adjust multiplier based on utilization match utilization_rate { - 0..=30 => 90, // Low utilization - discount - 31..=50 => 100, // Normal utilization - 51..=70 => 115, // Medium-high utilization - slight increase - 71..=85 => 135, // High utilization - significant increase - _ => 160, // Critical utilization - major increase + 0..=30 => 90, // Low utilization - discount + 31..=50 => 100, // Normal utilization + 51..=70 => 115, // Medium-high utilization - slight increase + 71..=85 => 135, // High utilization - significant increase + _ => 160, // Critical utilization - major increase } } @@ -245,7 +248,7 @@ fn calculate_pool_utilization_multiplier(pool: &RiskPool) -> u32 { /// Longer policies get slight discounts for stability fn calculate_time_multiplier(duration_seconds: u64) -> u32 { match duration_seconds { - 0..=2_592_000 => 105, // < 30 days - short term premium + 0..=2_592_000 => 105, // < 30 days - short term premium 2_592_001..=7_776_000 => 100, // 1-3 months - standard 7_776_001..=15_552_000 => 95, // 3-6 months - slight discount 15_552_001..=31_536_000 => 90, // 6-12 months - good discount @@ -289,10 +292,10 @@ fn calculate_discount_multiplier(modifiers: &PremiumModifiers) -> u32 { // Claim-free discount (up to 20% based on years) if modifiers.claim_free_years > 0 { let claim_free_discount = match modifiers.claim_free_years { - 1 => 500, // 5% - 2 => 1000, // 10% - 3 => 1500, // 15% - _ => 2000, // 20% for 4+ years + 1 => 500, // 5% + 2 => 1000, // 10% + 3 => 1500, // 15% + _ => 2000, // 20% for 4+ years }; total_discount_bps = total_discount_bps.saturating_add(claim_free_discount); } @@ -305,9 +308,9 @@ fn calculate_discount_multiplier(modifiers: &PremiumModifiers) -> u32 { // Loyalty discount (up to 10%) if modifiers.loyalty_years > 0 { let loyalty_discount = match modifiers.loyalty_years { - 1..=2 => 300, // 3% - 3..=5 => 600, // 6% - _ => 1000, // 10% for 6+ years + 1..=2 => 300, // 3% + 3..=5 => 600, // 6% + _ => 1000, // 10% for 6+ years }; total_discount_bps = total_discount_bps.saturating_add(loyalty_discount); } @@ -332,11 +335,11 @@ fn calculate_deductible( // Adjust based on risk (higher risk = higher deductible) let risk_adjustment: u32 = match assessment.overall_risk_score { - 0..=20 => 200, // Very high risk - 20% deductible - 21..=40 => 150, // High risk - 15% - 41..=60 => 100, // Medium risk - 10% - 61..=80 => 75, // Low risk - 7.5% - _ => 50, // Very low risk - 5% + 0..=20 => 200, // Very high risk - 20% deductible + 21..=40 => 150, // High risk - 15% + 41..=60 => 100, // Medium risk - 10% + 61..=80 => 75, // Low risk - 7.5% + _ => 50, // Very low risk - 5% }; let deductible_rate = base_deductible_rate.saturating_add(risk_adjustment); @@ -353,13 +356,10 @@ fn calculate_deductible( } /// Calculate risk adjustment amount for breakdown -fn calculate_risk_adjustment_amount( - coverage: u128, - base_rate: u32, - risk_multiplier: u32, -) -> u128 { +fn calculate_risk_adjustment_amount(coverage: u128, base_rate: u32, risk_multiplier: u32) -> u128 { let base_premium = coverage.saturating_mul(base_rate as u128) / BASIS_POINTS_DENOMINATOR; - let risk_adjusted = base_premium.saturating_mul(risk_multiplier as u128) / BASIS_POINTS_DENOMINATOR; + let risk_adjusted = + base_premium.saturating_mul(risk_multiplier as u128) / BASIS_POINTS_DENOMINATOR; risk_adjusted.saturating_sub(base_premium) } @@ -375,9 +375,8 @@ fn calculate_coverage_adjustment_amount( .saturating_mul(risk_multiplier as u128) / PREMIUM_CALCULATION_DIVISOR; - let premium_after = premium_before - .saturating_mul(coverage_multiplier as u128) - / BASIS_POINTS_DENOMINATOR; + let premium_after = + premium_before.saturating_mul(coverage_multiplier as u128) / BASIS_POINTS_DENOMINATOR; premium_after.saturating_sub(premium_before) } @@ -396,9 +395,8 @@ fn calculate_pool_adjustment_amount( .saturating_mul(coverage_multiplier as u128) / PREMIUM_CALCULATION_DIVISOR; - let premium_after = premium_before - .saturating_mul(pool_multiplier as u128) - / BASIS_POINTS_DENOMINATOR; + let premium_after = + premium_before.saturating_mul(pool_multiplier as u128) / BASIS_POINTS_DENOMINATOR; premium_after.saturating_sub(premium_before) } @@ -419,9 +417,8 @@ fn calculate_time_adjustment_amount( .saturating_mul(pool_multiplier as u128) / PREMIUM_CALCULATION_DIVISOR_LARGE; - let premium_after = premium_before - .saturating_mul(time_multiplier as u128) - / BASIS_POINTS_DENOMINATOR; + let premium_after = + premium_before.saturating_mul(time_multiplier as u128) / BASIS_POINTS_DENOMINATOR; premium_after.saturating_sub(premium_before) } @@ -444,8 +441,7 @@ fn calculate_discount_amount( .saturating_mul(time_multiplier as u128) / PREMIUM_CALCULATION_DIVISOR_5MULT; - let final_premium = premium_before_discount - .saturating_mul(discount_multiplier as u128) + let final_premium = premium_before_discount.saturating_mul(discount_multiplier as u128) / BASIS_POINTS_DENOMINATOR; premium_before_discount.saturating_sub(final_premium) diff --git a/contracts/insurance/src/tests.rs b/contracts/insurance/src/tests.rs index 44c10e88..c297ed57 100644 --- a/contracts/insurance/src/tests.rs +++ b/contracts/insurance/src/tests.rs @@ -923,16 +923,16 @@ mod insurance_tests { fn test_assess_property_risk_comprehensive_works() { let mut contract = setup(); let result = contract.assess_property_risk_comprehensive( - 1, // property_id - 10, // property_age_years - 5_000_000_000_000u128, // property_value + 1, // property_id + 10, // property_age_years + 5_000_000_000_000u128, // property_value "premium_safe_zone".into(), // location_code - "steel_frame".into(), // construction_type - true, // has_security_system - true, // has_fire_extinguisher - true, // has_alarm_system - 45, // owner_age_years - 15, // years_as_owner + "steel_frame".into(), // construction_type + true, // has_security_system + true, // has_fire_extinguisher + true, // has_alarm_system + 45, // owner_age_years + 15, // years_as_owner ); assert!(result.is_ok()); let (risk_id, premium_multiplier) = result.unwrap(); @@ -948,7 +948,7 @@ mod insurance_tests { let (risk_id, multiplier) = contract .assess_property_risk_comprehensive( 1, - 5, // New property + 5, // New property 5_000_000_000_000u128, "premium_safe_zone".into(), "steel_frame".into(), @@ -962,7 +962,10 @@ mod insurance_tests { let model = contract.get_property_risk_model(risk_id).unwrap(); assert!(model.overall_risk_score < 400); // Should be low risk - assert_eq!(model.final_risk_level, crate::propchain_insurance::RiskLevel::VeryLow); + assert_eq!( + model.final_risk_level, + crate::propchain_insurance::RiskLevel::VeryLow + ); } #[ink::test] @@ -972,7 +975,7 @@ mod insurance_tests { let (risk_id, multiplier) = contract .assess_property_risk_comprehensive( 2, - 80, // Very old + 80, // Very old 500_000_000_000u128, // Low value "high_risk_zone".into(), "wood_frame".into(), @@ -986,7 +989,10 @@ mod insurance_tests { let model = contract.get_property_risk_model(risk_id).unwrap(); assert!(model.overall_risk_score > 600); // Should be high risk - assert_eq!(model.final_risk_level, crate::propchain_insurance::RiskLevel::High); + assert_eq!( + model.final_risk_level, + crate::propchain_insurance::RiskLevel::High + ); } #[ink::test] @@ -1013,11 +1019,9 @@ mod insurance_tests { // Now update with safety features added let (new_score, new_multiplier) = contract .update_property_risk_assessment( - risk_id, - 20, - true, // Added security system - true, // Added fire extinguisher - true, // Added alarm system + risk_id, 20, true, // Added security system + true, // Added fire extinguisher + true, // Added alarm system ) .unwrap(); @@ -1031,8 +1035,16 @@ mod insurance_tests { let accounts = test::default_accounts::(); test::set_caller::(accounts.bob); let result = contract.assess_property_risk_comprehensive( - 1, 10, 1_000_000_000_000u128, "suburban".into(), "concrete".into(), - true, true, true, 40, 5, + 1, + 10, + 1_000_000_000_000u128, + "suburban".into(), + "concrete".into(), + true, + true, + true, + 40, + 5, ); assert_eq!(result, Err(InsuranceError::Unauthorized)); } @@ -1085,7 +1097,7 @@ mod insurance_tests { let result = contract.assess_claim_fraud_risk(claim_id, policy_id); assert!(result.is_ok()); let (assessment_id, fraud_score, requires_review) = result.unwrap(); - + // Low risk claim should have low fraud score assert!(fraud_score < 450); // Below medium threshold } @@ -1178,7 +1190,9 @@ mod insurance_tests { .unwrap(); test::set_caller::(accounts.alice); - let (assessment_id, _, _) = contract.assess_claim_fraud_risk(claim_id, policy_id).unwrap(); + let (assessment_id, _, _) = contract + .assess_claim_fraud_risk(claim_id, policy_id) + .unwrap(); // Retrieve the assessment let assessment = contract.get_fraud_assessment(assessment_id).unwrap(); @@ -1667,10 +1681,8 @@ mod insurance_tests { #[cfg(test)] mod circuit_breaker_tests { + use crate::propchain_insurance::{CoverageType, InsuranceError, PropertyInsurance}; use ink::env::{test, DefaultEnvironment}; - use crate::propchain_insurance::{ - CoverageType, InsuranceError, PropertyInsurance, - }; fn setup_with_pool() -> (PropertyInsurance, u64) { let accounts = test::default_accounts::(); @@ -1728,9 +1740,7 @@ mod circuit_breaker_tests { fn test_circuit_breaker_admin_reset() { let (mut contract, _) = setup_with_pool(); // Manually trip by setting active - contract - .set_circuit_breaker_params(1, 1, 86_400) - .unwrap(); + contract.set_circuit_breaker_params(1, 1, 86_400).unwrap(); // Reset should work for admin assert!(contract.reset_circuit_breaker().is_ok()); assert!(!contract.is_circuit_breaker_active()); @@ -1767,8 +1777,8 @@ mod circuit_breaker_tests { #[cfg(test)] mod insurance_admin_rotation_tests { - use ink::env::{test, DefaultEnvironment}; use crate::propchain_insurance::{InsuranceError, PropertyInsurance}; + use ink::env::{test, DefaultEnvironment}; fn setup() -> PropertyInsurance { let accounts = test::default_accounts::(); diff --git a/contracts/tax-compliance/src/jurisdiction_presets.rs b/contracts/tax-compliance/src/jurisdiction_presets.rs index 6fa2ba86..0ee6c463 100644 --- a/contracts/tax-compliance/src/jurisdiction_presets.rs +++ b/contracts/tax-compliance/src/jurisdiction_presets.rs @@ -8,7 +8,7 @@ pub fn us_federal_rule() -> TaxRule { TaxRule { rate_basis_points: 300, // 3% property tax fixed_charge: 500, - exemption_amount: 50_000, // Homestead exemption + exemption_amount: 50_000, // Homestead exemption payment_due_period: 90 * 24 * 60 * 60 * 1000, // 90 days reporting_frequency: ReportingFrequency::Annual, penalty_basis_points: 500, // 5% penalty @@ -26,10 +26,10 @@ pub fn us_federal_rule() -> TaxRule { /// - 30-day grace period pub fn us_federal_profile() -> JurisdictionProfile { JurisdictionProfile { - surcharge_basis_points: 100, // 1% local surcharge - early_payment_discount_basis_points: 150, // 1.5% early payment discount + surcharge_basis_points: 100, // 1% local surcharge + early_payment_discount_basis_points: 150, // 1.5% early payment discount late_payment_grace_period: 30 * 24 * 60 * 60 * 1000, // 30 days grace - optimization_window: 60 * 24 * 60 * 60 * 1000, // 60 days for early payment + optimization_window: 60 * 24 * 60 * 60 * 1000, // 60 days for early payment requires_digital_stamp: false, authority_hash: [0u8; 32], } @@ -63,7 +63,7 @@ pub fn eu_standard_rule() -> TaxRule { /// - Digital stamp required (GDPR compliance) pub fn eu_standard_profile() -> JurisdictionProfile { JurisdictionProfile { - surcharge_basis_points: 50, // 0.5% municipal surcharge + surcharge_basis_points: 50, // 0.5% municipal surcharge early_payment_discount_basis_points: 200, // 2% GDPR-compliant early payment late_payment_grace_period: 15 * 24 * 60 * 60 * 1000, // 15 days (stricter) optimization_window: 45 * 24 * 60 * 60 * 1000, @@ -100,7 +100,7 @@ pub fn asia_standard_rule() -> TaxRule { /// - Digital stamp required pub fn asia_standard_profile() -> JurisdictionProfile { JurisdictionProfile { - surcharge_basis_points: 150, // 1.5% local development charge + surcharge_basis_points: 150, // 1.5% local development charge early_payment_discount_basis_points: 100, // 1% early payment late_payment_grace_period: 20 * 24 * 60 * 60 * 1000, optimization_window: 50 * 24 * 60 * 60 * 1000, diff --git a/contracts/tax-compliance/src/lib.rs b/contracts/tax-compliance/src/lib.rs index 1ecf06ea..a3fe2f54 100644 --- a/contracts/tax-compliance/src/lib.rs +++ b/contracts/tax-compliance/src/lib.rs @@ -6,6 +6,10 @@ use propchain_contracts::{non_reentrant, ReentrancyError, ReentrancyGuard}; use propchain_traits::ComplianceChecker; use propchain_traits::*; +mod jurisdiction_presets; +mod tax_engine; +mod tax_strategies; + #[ink::contract] mod tax_compliance { use super::*; @@ -512,17 +516,17 @@ mod tax_compliance { verified: bool, } -#[ink(event)] -pub struct ComplianceRegistrySyncRequested { - #[ink(topic)] - property_id: u64, - #[ink(topic)] - jurisdiction_code: u32, - reporting_period: u64, - outstanding_tax: Balance, - legal_documents_verified: bool, - reporting_submitted: bool, -} + #[ink(event)] + pub struct ComplianceRegistrySyncRequested { + #[ink(topic)] + property_id: u64, + #[ink(topic)] + jurisdiction_code: u32, + reporting_period: u64, + outstanding_tax: Balance, + legal_documents_verified: bool, + reporting_submitted: bool, + } #[ink(event)] pub struct ComplianceCacheHit { @@ -545,28 +549,28 @@ pub enum DeadlineAlertLevel { Urgent, } -#[ink(event)] -pub struct TaxDeadlineApproaching { - #[ink(topic)] - property_id: u64, - #[ink(topic)] - jurisdiction_code: u32, - reporting_period: u64, - due_at: Timestamp, - days_remaining: u16, - alert_level: DeadlineAlertLevel, -} + #[ink(event)] + pub struct TaxDeadlineApproaching { + #[ink(topic)] + property_id: u64, + #[ink(topic)] + jurisdiction_code: u32, + reporting_period: u64, + due_at: Timestamp, + days_remaining: u16, + alert_level: DeadlineAlertLevel, + } -#[ink(event)] -pub struct TaxDeadlineNotification { - #[ink(topic)] - property_id: u64, - #[ink(topic)] - jurisdiction_code: u32, - reporting_period: u64, - due_at: Timestamp, - days_remaining: u16, -} + #[ink(event)] + pub struct TaxDeadlineNotification { + #[ink(topic)] + property_id: u64, + #[ink(topic)] + jurisdiction_code: u32, + reporting_period: u64, + due_at: Timestamp, + days_remaining: u16, + } #[ink(event)] pub struct TaxDocumentUploaded { @@ -762,7 +766,8 @@ pub struct TaxDeadlineNotification { profile: JurisdictionProfile, ) -> Result<()> { self.ensure_admin()?; - self.jurisdiction_profiles.insert(jurisdiction.code, &profile); + self.jurisdiction_profiles + .insert(jurisdiction.code, &profile); self.log_audit( 0, jurisdiction.code, @@ -924,7 +929,11 @@ pub struct TaxDeadlineNotification { // Emit tax deadline notification if approaching if let Some(days) = days_until_due(now, record.due_at) { if days <= 30 { - let alert_level = if days <= 7 { DeadlineAlertLevel::Urgent } else { DeadlineAlertLevel::Approaching }; + let alert_level = if days <= 7 { + DeadlineAlertLevel::Urgent + } else { + DeadlineAlertLevel::Approaching + }; self.env().emit_event(TaxDeadlineApproaching { property_id, jurisdiction_code: jurisdiction.code, @@ -1150,7 +1159,11 @@ pub struct TaxDeadlineNotification { if let Some(record) = record { if let Some(days) = days_until_due(now, record.due_at) { if days <= 30 { - let alert_level = if days <= 7 { DeadlineAlertLevel::Urgent } else { DeadlineAlertLevel::Approaching }; + let alert_level = if days <= 7 { + DeadlineAlertLevel::Urgent + } else { + DeadlineAlertLevel::Approaching + }; self.env().emit_event(TaxDeadlineApproaching { property_id, jurisdiction_code: jurisdiction.code, @@ -1397,7 +1410,10 @@ pub struct TaxDeadlineNotification { } #[ink(message)] - pub fn get_jurisdiction_profile(&self, jurisdiction_code: u32) -> Option { + pub fn get_jurisdiction_profile( + &self, + jurisdiction_code: u32, + ) -> Option { self.jurisdiction_profiles.get(jurisdiction_code) } @@ -1464,9 +1480,9 @@ pub struct TaxDeadlineNotification { let current_taxable_value = self.taxable_value(&rule, &assessment); let mut opportunities = Vec::new(); - if let Some(record) = self - .tax_records - .get((property_id, jurisdiction.code, reporting_period)) + if let Some(record) = + self.tax_records + .get((property_id, jurisdiction.code, reporting_period)) { let previous_base_due = self .base_tax_due(record.taxable_value, rule.rate_basis_points) @@ -1493,10 +1509,9 @@ pub struct TaxDeadlineNotification { let exemption_threshold = assessment.assessed_value / 20; if assessment.exemption_override < exemption_threshold { - let revised_taxable_value = assessment.assessed_value.saturating_sub( - rule.exemption_amount - .saturating_add(exemption_threshold), - ); + let revised_taxable_value = assessment + .assessed_value + .saturating_sub(rule.exemption_amount.saturating_add(exemption_threshold)); let revised_tax_due = self .base_tax_due(revised_taxable_value, rule.rate_basis_points) .saturating_add(rule.fixed_charge); @@ -1515,7 +1530,8 @@ pub struct TaxDeadlineNotification { } } - opportunities.sort_by(|left, right| right.estimated_savings.cmp(&left.estimated_savings)); + opportunities + .sort_by(|left, right| right.estimated_savings.cmp(&left.estimated_savings)); Ok(opportunities) } @@ -1530,7 +1546,9 @@ pub struct TaxDeadlineNotification { let profile = self.jurisdiction_profiles.get(jurisdiction.code); let now = self.env().block_timestamp(); let reporting_period = self.reporting_period(now, rule.reporting_frequency); - let record = self.tax_records.get((property_id, jurisdiction.code, reporting_period)); + let record = self + .tax_records + .get((property_id, jurisdiction.code, reporting_period)); Ok(calculate_timing_strategy(rule, profile, record, now)) } @@ -1628,7 +1646,9 @@ pub struct TaxDeadlineNotification { .ok_or(Error::AssessmentNotFound)?; let now = self.env().block_timestamp(); let reporting_period = self.reporting_period(now, rule.reporting_frequency); - let record = self.tax_records.get((property_id, jurisdiction.code, reporting_period)); + let record = self + .tax_records + .get((property_id, jurisdiction.code, reporting_period)); Ok(analyze_strategies( rule, @@ -1663,7 +1683,11 @@ pub struct TaxDeadlineNotification { /// Canonical key for a treaty: (min, max) so order of arguments doesn't matter. fn treaty_key(a: u32, b: u32) -> (u32, u32) { - if a <= b { (a, b) } else { (b, a) } + if a <= b { + (a, b) + } else { + (b, a) + } } fn get_active_rule(&self, jurisdiction_code: u32) -> Result { @@ -1695,9 +1719,10 @@ pub struct TaxDeadlineNotification { } fn taxable_value(&self, rule: &TaxRule, assessment: &PropertyAssessment) -> Balance { - assessment - .assessed_value - .saturating_sub(rule.exemption_amount.saturating_add(assessment.exemption_override)) + assessment.assessed_value.saturating_sub( + rule.exemption_amount + .saturating_add(assessment.exemption_override), + ) } fn base_tax_due(&self, taxable_value: Balance, rate_basis_points: u32) -> Balance { @@ -2219,7 +2244,9 @@ pub struct TaxDeadlineNotification { .set_property_assessment(7, jurisdiction(), owner, 200_000, 5_000) .expect("assessment"); - let record = contract.calculate_tax(7, jurisdiction(), None).expect("tax"); + let record = contract + .calculate_tax(7, jurisdiction(), None) + .expect("tax"); assert_eq!(record.taxable_value, 185_000); assert_eq!(record.tax_due, 5_625); assert_eq!(record.status, TaxStatus::Assessed); @@ -2256,7 +2283,10 @@ pub struct TaxDeadlineNotification { assert!(reassessment.estimated_savings > 0); assert!(reassessment.current_tax_due >= reassessment.revised_tax_due); - assert_eq!(reassessment.reporting_period, initial_record.reporting_period); + assert_eq!( + reassessment.reporting_period, + initial_record.reporting_period + ); } #[ink::test] @@ -2271,7 +2301,9 @@ pub struct TaxDeadlineNotification { .set_property_assessment(8, jurisdiction(), owner, 120_000, 0) .expect("assessment"); - let record = contract.calculate_tax(8, jurisdiction(), None).expect("tax"); + let record = contract + .calculate_tax(8, jurisdiction(), None) + .expect("tax"); let initial = contract .check_compliance(8, jurisdiction()) .expect("compliance"); @@ -2314,7 +2346,9 @@ pub struct TaxDeadlineNotification { contract .set_property_assessment(9, jurisdiction(), owner, 100_000, 0) .expect("assessment"); - let record = contract.calculate_tax(9, jurisdiction(), None).expect("tax"); + let record = contract + .calculate_tax(9, jurisdiction(), None) + .expect("tax"); contract .record_tax_payment( 9, @@ -2424,10 +2458,7 @@ pub struct TaxDeadlineNotification { assert_eq!(treaty.reduction_basis_points, 2000); assert!(treaty.active); // Canonical key: same result regardless of argument order - assert_eq!( - contract.get_tax_treaty(2001, 1001), - Some(treaty) - ); + assert_eq!(contract.get_tax_treaty(2001, 1001), Some(treaty)); } #[ink::test] @@ -2435,7 +2466,9 @@ pub struct TaxDeadlineNotification { let mut contract = TaxComplianceModule::new(None); let owner = AccountId::from([0x10; 32]); - contract.configure_tax_rule(jurisdiction(), rule()).expect("rule"); + contract + .configure_tax_rule(jurisdiction(), rule()) + .expect("rule"); contract .set_property_assessment(20, jurisdiction(), owner, 200_000, 5_000) .expect("assessment"); @@ -2447,7 +2480,12 @@ pub struct TaxDeadlineNotification { // Set a 20 % reduction treaty contract - .set_tax_treaty(jurisdiction().code, residence_jurisdiction().code, 2000, true) + .set_tax_treaty( + jurisdiction().code, + residence_jurisdiction().code, + 2000, + true, + ) .expect("treaty"); let record_with_treaty = contract @@ -2455,10 +2493,7 @@ pub struct TaxDeadlineNotification { .expect("tax with treaty"); // tax_due should be 20 % less - let expected = record_no_treaty - .tax_due - .saturating_mul(8000) - / 10_000; + let expected = record_no_treaty.tax_due.saturating_mul(8000) / 10_000; assert_eq!(record_with_treaty.tax_due, expected); assert!(record_with_treaty.tax_due < record_no_treaty.tax_due); } @@ -2468,14 +2503,21 @@ pub struct TaxDeadlineNotification { let mut contract = TaxComplianceModule::new(None); let owner = AccountId::from([0x11; 32]); - contract.configure_tax_rule(jurisdiction(), rule()).expect("rule"); + contract + .configure_tax_rule(jurisdiction(), rule()) + .expect("rule"); contract .set_property_assessment(21, jurisdiction(), owner, 200_000, 0) .expect("assessment"); // Inactive treaty contract - .set_tax_treaty(jurisdiction().code, residence_jurisdiction().code, 3000, false) + .set_tax_treaty( + jurisdiction().code, + residence_jurisdiction().code, + 3000, + false, + ) .expect("treaty"); let record_no_treaty = contract diff --git a/contracts/tax-compliance/src/tax_strategies.rs b/contracts/tax-compliance/src/tax_strategies.rs index 3b6c4c87..41f784ed 100644 --- a/contracts/tax-compliance/src/tax_strategies.rs +++ b/contracts/tax-compliance/src/tax_strategies.rs @@ -195,7 +195,8 @@ pub(crate) fn calculate_timing_strategy( ) -> TimingStrategy { let early_payment_benefit = profile .map(|p| { - let base_tax = (1_000_000 * p.early_payment_discount_basis_points as Balance) / BASIS_POINTS_DENOMINATOR; + let base_tax = (1_000_000 * p.early_payment_discount_basis_points as Balance) + / BASIS_POINTS_DENOMINATOR; base_tax }) .unwrap_or(0); @@ -210,7 +211,9 @@ pub(crate) fn calculate_timing_strategy( }) .unwrap_or(0); - let optimization_window = profile.map(|p| p.optimization_window).unwrap_or(30 * 24 * 60 * 60 * 1000); + let optimization_window = profile + .map(|p| p.optimization_window) + .unwrap_or(30 * 24 * 60 * 60 * 1000); let installment_count = if penalty_avoidance > 0 { 2 } else { 1 }; TimingStrategy { @@ -228,7 +231,8 @@ pub(crate) fn calculate_transfer_strategy( rule: TaxRule, ) -> TransferStrategy { let basis_adjustment = assessment.assessed_value / 10; - let transfer_tax_savings = (basis_adjustment * rule.rate_basis_points as Balance) / BASIS_POINTS_DENOMINATOR; + let transfer_tax_savings = + (basis_adjustment * rule.rate_basis_points as Balance) / BASIS_POINTS_DENOMINATOR; TransferStrategy { multi_step_transfer: assessment.assessed_value > 1_000_000, @@ -264,7 +268,8 @@ pub(crate) fn calculate_entity_strategy( ) -> EntityStrategy { // LLC typically has ~20% lower tax rate than individual let recommended_rate = (current_tax_rate * 80) / 100; - let annual_tax_base = (assessment_value * current_tax_rate as Balance) / BASIS_POINTS_DENOMINATOR; + let annual_tax_base = + (assessment_value * current_tax_rate as Balance) / BASIS_POINTS_DENOMINATOR; let optimized_tax = (assessment_value * recommended_rate as Balance) / BASIS_POINTS_DENOMINATOR; let savings = annual_tax_base.saturating_sub(optimized_tax); let restructuring_cost = assessment_value / 100; // ~1% of property value @@ -279,10 +284,14 @@ pub(crate) fn calculate_entity_strategy( } /// Calculates installment strategy for transaction structuring -pub(crate) fn calculate_installment_strategy( - total_amount: Balance, -) -> InstallmentStrategy { - let installment_count = if total_amount > 1_000_000 { 4 } else if total_amount > 500_000 { 3 } else { 2 }; +pub(crate) fn calculate_installment_strategy(total_amount: Balance) -> InstallmentStrategy { + let installment_count = if total_amount > 1_000_000 { + 4 + } else if total_amount > 500_000 { + 3 + } else { + 2 + }; let amount_per_installment = total_amount / installment_count as Balance; let spacing = 90 * 24 * 60 * 60 * 1000; // 90 days let fees = (total_amount * 50) / BASIS_POINTS_DENOMINATOR; // 0.5% fees @@ -310,7 +319,7 @@ pub(crate) fn calculate_cross_border_strategy( let optimized_combined = (current_combined * 75) / 100; let rate_reduction = current_combined.saturating_sub(optimized_combined); let treaty_savings = (transaction_value * rate_reduction as Balance) / BASIS_POINTS_DENOMINATOR; - + // Transfer pricing opportunity ~5% of transaction value let transfer_pricing = (transaction_value * 500) / BASIS_POINTS_DENOMINATOR; @@ -341,7 +350,8 @@ pub(crate) fn analyze_strategies( let entity = calculate_entity_strategy(rule.rate_basis_points, assessment.assessed_value); // Calculate total savings - let total_savings = timing.estimated_savings + let total_savings = timing + .estimated_savings .saturating_add(transfer.estimated_savings) .saturating_add(portfolio.estimated_savings) .saturating_add(entity.estimated_annual_savings); @@ -389,7 +399,13 @@ pub(crate) fn analyze_strategies( let complexity = core::cmp::min(10, (applicable_strategies as u8) * 2); let risk = if profile.is_some() { 4 } else { 6 }; // Higher risk without jurisdiction profile - let priority = if timing.penalty_avoidance > 0 { 10 } else if applicable_strategies > 2 { 8 } else { 5 }; + let priority = if timing.penalty_avoidance > 0 { + 10 + } else if applicable_strategies > 2 { + 8 + } else { + 5 + }; OptimizationAnalysis { total_savings, @@ -498,7 +514,8 @@ mod tests { let property_count = 10; let harvesting_opportunity = 100_000; - let strategy = calculate_portfolio_strategy(total_value, property_count, harvesting_opportunity); + let strategy = + calculate_portfolio_strategy(total_value, property_count, harvesting_opportunity); assert_eq!(strategy.portfolio_value, total_value); assert_eq!(strategy.property_count, property_count);