diff --git a/contracts/lib/src/lib.rs b/contracts/lib/src/lib.rs index a94ff3d..de3e309 100644 --- a/contracts/lib/src/lib.rs +++ b/contracts/lib/src/lib.rs @@ -84,6 +84,8 @@ mod propchain_contracts { pause_info: PauseInfo, /// Accounts authorized to pause the contract pause_guardians: Mapping, + /// Oracle contract address (optional) + oracle: Option, } /// Escrow information @@ -765,6 +767,7 @@ mod propchain_contracts { required_approvals: 2, // Default requirement }, pause_guardians: Mapping::default(), + oracle: None, }; // Emit contract initialization event @@ -790,6 +793,46 @@ mod propchain_contracts { self.admin } + /// Set the oracle contract address + #[ink(message)] + pub fn set_oracle(&mut self, oracle: AccountId) -> Result<(), Error> { + let caller = self.env().caller(); + if caller != self.admin { + return Err(Error::Unauthorized); + } + self.oracle = Some(oracle); + Ok(()) + } + + /// Returns the oracle contract address + #[ink(message)] + pub fn oracle(&self) -> Option { + self.oracle + } + + /// Update property valuation using the oracle + #[ink(message)] + pub fn update_valuation_from_oracle(&mut self, property_id: u64) -> Result<(), Error> { + let oracle_addr = self.oracle.ok_or(Error::OracleError)?; + + // Use the Oracle trait to perform the cross-contract call + use ink::env::call::FromAccountId; + let oracle: ink::contract_ref!(Oracle) = FromAccountId::from_account_id(oracle_addr); + + // Fetch valuation from oracle + let valuation = oracle.get_valuation(property_id).map_err(|_| Error::OracleError)?; + + // Update the property's recorded valuation in its metadata + if let Some(mut property) = self.properties.get(&property_id) { + property.metadata.valuation = valuation.valuation; + self.properties.insert(&property_id, &property); + } else { + return Err(Error::PropertyNotFound); + } + + Ok(()) + } + /// Changes the admin account (only callable by current admin) #[ink(message)] pub fn change_admin(&mut self, new_admin: AccountId) -> Result<(), Error> { diff --git a/contracts/oracle/README.md b/contracts/oracle/README.md index be67dd0..fd4106a 100644 --- a/contracts/oracle/README.md +++ b/contracts/oracle/README.md @@ -11,6 +11,10 @@ The Property Valuation Oracle provides real-time property valuations for the Pro - **Location-Based Adjustments**: Geographic market adjustments - **Price Alert System**: Notifications for significant valuation changes - **Fallback Mechanisms**: Redundant oracle sources for reliability +- **Oracle Reputation System**: Performance tracking and automated source management +- **Slashing System**: Stake-based penalties for malicious or inaccurate data +- **Gas-Efficient Batching**: Support for multiple property valuation requests in a single transaction +- **Anomaly Detection**: Advanced validation logic to detect market outliers ## Architecture @@ -48,6 +52,12 @@ Returns historical valuations (most recent first). #### `set_price_alert(property_id: u64, threshold_percentage: u32, alert_address: AccountId)` Sets up alerts for price changes exceeding the threshold. +#### `request_property_valuation(property_id: u64) -> Result` +Initiates a new valuation request for a property. + +#### `batch_request_valuations(property_ids: Vec) -> Result, OracleError>` +Batch requests valuations for multiple properties efficiently. + ### Administrative Functions #### `add_oracle_source(source: OracleSource) -> Result<(), OracleError>` @@ -59,6 +69,12 @@ Configures location-based valuation adjustments (admin only). #### `update_market_trend(trend: MarketTrend) -> Result<(), OracleError>` Updates market trend data for volatility calculations (admin only). +#### `update_source_reputation(source_id: String, success: bool) -> Result<(), OracleError>` +Manages oracle source reputation scores (admin only). + +#### `slash_source(source_id: String, penalty: u128) -> Result<(), OracleError>` +Slashes staked funds for underperforming or malicious sources (admin only). + ## Data Structures ### PropertyValuation diff --git a/contracts/oracle/src/lib.rs b/contracts/oracle/src/lib.rs index 860ebc6..2b912cb 100644 --- a/contracts/oracle/src/lib.rs +++ b/contracts/oracle/src/lib.rs @@ -19,19 +19,6 @@ mod propchain_oracle { vec::Vec, }; - /// Error types for the Property Valuation Oracle - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum OracleError { - PropertyNotFound, - InsufficientSources, - InvalidValuation, - Unauthorized, - OracleSourceNotFound, - InvalidParameters, - PriceFeedError, - AlertNotFound, - } /// Property Valuation Oracle storage #[ink(storage)] @@ -40,7 +27,7 @@ mod propchain_oracle { admin: AccountId, /// Property valuations storage - property_valuations: Mapping, + pub property_valuations: Mapping, /// Historical valuations per property historical_valuations: Mapping>, @@ -58,7 +45,7 @@ mod propchain_oracle { pub location_adjustments: Mapping, /// Market trends data - market_trends: Mapping, + pub market_trends: Mapping, /// Comparable properties cache comparable_cache: Mapping>, @@ -71,6 +58,18 @@ mod propchain_oracle { /// Outlier detection threshold (standard deviations) outlier_threshold: u32, + + /// Source reputations (0-1000, where 1000 is perfect) + pub source_reputations: Mapping, + + /// Source stakes for slashing + pub source_stakes: Mapping, + + /// Pending valuation requests: property_id -> timestamp + pub pending_requests: Mapping, + + /// Request counter for unique request IDs + pub request_id_counter: u64, } /// Events emitted by the oracle @@ -118,6 +117,10 @@ mod propchain_oracle { max_price_staleness: 3600, // 1 hour min_sources_required: 2, outlier_threshold: 2, // 2 standard deviations + source_reputations: Mapping::default(), + source_stakes: Mapping::default(), + pending_requests: Mapping::default(), + request_id_counter: 0, } } @@ -213,7 +216,97 @@ mod propchain_oracle { valuation_method: ValuationMethod::MarketData, }; - self.update_property_valuation(property_id, valuation) + self.update_property_valuation(property_id, valuation)?; + self.clear_pending_request(property_id); + Ok(()) + } + + /// Request a new valuation for a property + #[ink(message)] + pub fn request_property_valuation(&mut self, property_id: u64) -> Result { + // Check if request already pending + if let Some(timestamp) = self.pending_requests.get(&property_id) { + let current_time = self.env().block_timestamp(); + if current_time.saturating_sub(timestamp) < self.max_price_staleness { + return Err(OracleError::RequestPending); + } + } + + let request_id = self.request_id_counter; + self.request_id_counter += 1; + + self.pending_requests.insert(&property_id, &self.env().block_timestamp()); + + Ok(request_id) + } + + /// Batch request valuations for multiple properties + #[ink(message)] + pub fn batch_request_valuations(&mut self, property_ids: Vec) -> Result, OracleError> { + let mut request_ids = Vec::new(); + for id in property_ids { + if let Ok(req_id) = self.request_property_valuation(id) { + request_ids.push(req_id); + } + } + Ok(request_ids) + } + + /// Update oracle reputation (admin only) + #[ink(message)] + pub fn update_source_reputation(&mut self, source_id: String, success: bool) -> Result<(), OracleError> { + self.ensure_admin()?; + let current_rep = self.source_reputations.get(&source_id).unwrap_or(500); // Start at 500 + + let new_rep = if success { + (current_rep + 10).min(1000) + } else { + current_rep.saturating_sub(50) + }; + + self.source_reputations.insert(&source_id, &new_rep); + + // Auto-deactivate source if reputation falls too low + if new_rep < 200 { + if let Some(mut source) = self.oracle_sources.get(&source_id) { + source.is_active = false; + self.oracle_sources.insert(&source_id, &source); + self.active_sources.retain(|id| id != &source_id); + } + } + + Ok(()) + } + + /// Slash an oracle source for providing bad data (admin only) + #[ink(message)] + pub fn slash_source(&mut self, source_id: String, penalty: u128) -> Result<(), OracleError> { + self.ensure_admin()?; + + let current_stake = self.source_stakes.get(&source_id).unwrap_or(0); + self.source_stakes.insert(&source_id, ¤t_stake.saturating_sub(penalty)); + + // Also hit the reputation hard + self.update_source_reputation(source_id, false)?; + + Ok(()) + } + + /// Detect if a new valuation is an anomaly based on historical data + #[ink(message)] + pub fn is_anomaly(&self, property_id: u64, new_valuation: u128) -> bool { + if let Some(current) = self.property_valuations.get(&property_id) { + let change_pct = self.calculate_percentage_change(current.valuation, new_valuation); + + // If change > 20% in a single update, flag as anomaly unless volatility is high + if change_pct > 20 { + let volatility = self.calculate_volatility(property_id).unwrap_or(0); + if volatility < 10 { // 10% volatility + return true; + } + } + } + false } /// Get historical valuations for a property @@ -385,6 +478,10 @@ mod propchain_oracle { // Implement Pyth integration Err(OracleError::PriceFeedError) } + OracleSourceType::Substrate => { + // Implement Substrate price feed integration (pallets/OCW) + Err(OracleError::PriceFeedError) + } OracleSourceType::Manual => { // Manual price updates only Err(OracleError::PriceFeedError) @@ -632,6 +729,87 @@ mod propchain_oracle { (diff * 100) / old_value } + + /// Clear pending request after successful update + fn clear_pending_request(&mut self, property_id: u64) { + self.pending_requests.remove(&property_id); + } + } + + /// Implementation of the Oracle trait from propchain-traits + impl propchain_traits::Oracle for PropertyValuationOracle { + #[ink(message)] + fn get_valuation(&self, property_id: u64) -> Result { + self.get_property_valuation(property_id) + } + + #[ink(message)] + fn get_valuation_with_confidence( + &self, + property_id: u64, + ) -> Result { + self.get_valuation_with_confidence(property_id) + } + + #[ink(message)] + fn request_valuation(&mut self, property_id: u64) -> Result { + self.request_property_valuation(property_id) + } + + #[ink(message)] + fn batch_request_valuations(&mut self, property_ids: Vec) -> Result, OracleError> { + self.batch_request_valuations(property_ids) + } + + #[ink(message)] + fn get_historical_valuations(&self, property_id: u64, limit: u32) -> Vec { + self.get_historical_valuations(property_id, limit) + } + + #[ink(message)] + fn get_market_volatility( + &self, + property_type: PropertyType, + location: String, + ) -> Result { + self.get_market_volatility(property_type, location) + } + } + + /// Implementation of the OracleRegistry trait from propchain-traits + impl propchain_traits::OracleRegistry for PropertyValuationOracle { + #[ink(message)] + fn add_source(&mut self, source: OracleSource) -> Result<(), OracleError> { + self.add_oracle_source(source) + } + + #[ink(message)] + fn remove_source(&mut self, source_id: String) -> Result<(), OracleError> { + self.ensure_admin()?; + self.oracle_sources.remove(&source_id); + self.active_sources.retain(|id| id != &source_id); + Ok(()) + } + + #[ink(message)] + fn update_reputation(&mut self, source_id: String, success: bool) -> Result<(), OracleError> { + self.update_source_reputation(source_id, success) + } + + #[ink(message)] + fn get_reputation(&self, source_id: String) -> Option { + self.source_reputations.get(&source_id) + } + + #[ink(message)] + fn slash_source(&mut self, source_id: String, penalty_amount: u128) -> Result<(), OracleError> { + self.slash_source(source_id, penalty_amount) + } + + #[ink(message)] + fn detect_anomalies(&self, property_id: u64, new_valuation: u128) -> bool { + self.is_anomaly(property_id, new_valuation) + } } impl Default for PropertyValuationOracle { @@ -641,9 +819,9 @@ mod propchain_oracle { } } -// Re-export the contract // Re-export the contract and error type -pub use propchain_oracle::{OracleError, PropertyValuationOracle}; +pub use propchain_traits::OracleError; +pub use propchain_oracle::PropertyValuationOracle; #[cfg(test)] mod oracle_tests { @@ -937,4 +1115,69 @@ mod oracle_tests { let result = oracle.aggregate_prices(&prices); assert_eq!(result, Err(OracleError::InsufficientSources)); } + + #[ink::test] + fn test_source_reputation_works() { + let mut oracle = setup_oracle(); + let source_id = "source1".to_string(); + + // Initial reputation should be 500 + assert!(oracle.update_source_reputation(source_id.clone(), true).is_ok()); + assert_eq!(oracle.source_reputations.get(&source_id).unwrap(), 510); + + // Test penalty + assert!(oracle.update_source_reputation(source_id.clone(), false).is_ok()); + assert_eq!(oracle.source_reputations.get(&source_id).unwrap(), 460); + } + + #[ink::test] + fn test_slashing_works() { + let mut oracle = setup_oracle(); + let source_id = "source1".to_string(); + + oracle.source_stakes.insert(&source_id, &1000); + assert!(oracle.slash_source(source_id.clone(), 100).is_ok()); + + assert_eq!(oracle.source_stakes.get(&source_id).unwrap(), 900); + // Reputation should also decrease + assert!(oracle.source_reputations.get(&source_id).unwrap() < 500); + } + + #[ink::test] + fn test_anomaly_detection_works() { + let mut oracle = setup_oracle(); + let property_id = 1; + + let valuation = PropertyValuation { + property_id, + valuation: 100000, + confidence_score: 90, + sources_used: 3, + last_updated: 0, + valuation_method: ValuationMethod::Automated, + }; + + oracle.property_valuations.insert(&property_id, &valuation); + + // Normal price change (5%) + assert!(!oracle.is_anomaly(property_id, 105000)); + + // Anomaly price change (25%) + assert!(oracle.is_anomaly(property_id, 130000)); + } + + #[ink::test] + fn test_batch_request_works() { + let mut oracle = setup_oracle(); + let property_ids = vec![1, 2, 3]; + + let result = oracle.batch_request_valuations(property_ids); + assert!(result.is_ok()); + let request_ids = result.unwrap(); + assert_eq!(request_ids.len(), 3); + + assert!(oracle.pending_requests.get(&1).is_some()); + assert!(oracle.pending_requests.get(&2).is_some()); + assert!(oracle.pending_requests.get(&3).is_some()); + } } diff --git a/contracts/traits/src/lib.rs b/contracts/traits/src/lib.rs index c7f6ccf..bfd1ee5 100644 --- a/contracts/traits/src/lib.rs +++ b/contracts/traits/src/lib.rs @@ -3,6 +3,23 @@ use ink::prelude::string::String; use ink::primitives::AccountId; +/// Error types for the Property Valuation Oracle +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum OracleError { + PropertyNotFound, + InsufficientSources, + InvalidValuation, + Unauthorized, + OracleSourceNotFound, + InvalidParameters, + PriceFeedError, + AlertNotFound, + InsufficientReputation, + SourceAlreadyExists, + RequestPending, +} + /// Trait definitions for PropChain contracts pub trait PropertyRegistry { /// Error type for the contract @@ -195,6 +212,7 @@ pub struct OracleSource { pub enum OracleSourceType { Chainlink, Pyth, + Substrate, Custom, Manual, } @@ -226,6 +244,69 @@ pub struct MarketTrend { pub last_updated: u64, } +/// Oracle trait for real-time property valuation +#[ink::trait_definition] +pub trait Oracle { + /// Get current property valuation + #[ink(message)] + fn get_valuation(&self, property_id: u64) -> Result; + + /// Get valuation with detailed confidence metrics + #[ink(message)] + fn get_valuation_with_confidence( + &self, + property_id: u64, + ) -> Result; + + /// Request a new valuation for a property (async pattern) + #[ink(message)] + fn request_valuation(&mut self, property_id: u64) -> Result; + + /// Batch request valuations for multiple properties + #[ink(message)] + fn batch_request_valuations(&mut self, property_ids: Vec) -> Result, OracleError>; + + /// Get historical valuations for a property + #[ink(message)] + fn get_historical_valuations(&self, property_id: u64, limit: u32) -> Vec; + + /// Get market volatility for a specific location and property type + #[ink(message)] + fn get_market_volatility( + &self, + property_type: PropertyType, + location: String, + ) -> Result; +} + +/// Oracle Registry trait for managing multiple price feeds and reputation +#[ink::trait_definition] +pub trait OracleRegistry { + /// Register a new oracle source + #[ink(message)] + fn add_source(&mut self, source: OracleSource) -> Result<(), OracleError>; + + /// Remove an oracle source + #[ink(message)] + fn remove_source(&mut self, source_id: String) -> Result<(), OracleError>; + + /// Update oracle source reputation based on performance + #[ink(message)] + fn update_reputation(&mut self, source_id: String, success: bool) -> Result<(), OracleError>; + + /// Get oracle source reputation score + #[ink(message)] + fn get_reputation(&self, source_id: String) -> Option; + + /// Slash oracle source for providing invalid data + #[ink(message)] + fn slash_source(&mut self, source_id: String, penalty_amount: u128) -> Result<(), OracleError>; + + /// Check for anomalies in price data + #[ink(message)] + fn detect_anomalies(&self, property_id: u64, new_valuation: u128) -> bool; +} + /// Escrow trait for secure property transfers pub trait Escrow { /// Error type for escrow operations