diff --git a/contracts/price-oracle/src/lib.rs b/contracts/price-oracle/src/lib.rs index f36c9c7..a879f4c 100644 --- a/contracts/price-oracle/src/lib.rs +++ b/contracts/price-oracle/src/lib.rs @@ -221,6 +221,24 @@ pub trait StellarFlowTrait { /// /// Returns true if the contract is frozen, false otherwise. fn is_frozen(env: Env) -> bool; + + /// Add a trusted external oracle for secondary validation. + /// + /// Only admins can add external oracles. The oracle will be used for price comparison. + fn add_external_oracle(env: Env, admin: Address, oracle: crate::types::ExternalOracle) -> Result<(), Error>; + + /// Remove a trusted external oracle. + /// + /// Only admins can remove external oracles. + fn remove_external_oracle(env: Env, admin: Address, oracle_name: Symbol) -> Result<(), Error>; + + /// Get all configured external oracles. + fn get_external_oracles(env: Env) -> soroban_sdk::Vec; + + /// Compare internal price with external oracle price. + /// + /// Returns the price comparison data including delta and whether it exceeds threshold. + fn compare_with_external(env: Env, asset: Symbol, oracle_name: Symbol) -> Result; } #[contractclient(name = "TokenContractClient")] @@ -2156,6 +2174,128 @@ impl PriceOracle { pub fn get_price_update_subscribers(env: Env) -> soroban_sdk::Vec
{ callbacks::get_subscribers(&env) } + + /// Add a trusted external oracle for secondary validation. + /// + /// Only admins can add external oracles. The oracle will be used for price comparison. + pub fn add_external_oracle(env: Env, admin: Address, oracle: crate::types::ExternalOracle) -> Result<(), Error> { + _require_not_destroyed(&env); + crate::auth::_require_not_frozen(&env); + admin.require_auth(); + crate::auth::_require_authorized(&env, &admin); + + let mut oracles: soroban_sdk::Vec = env + .storage() + .persistent() + .get(&DataKey::ExternalOracles) + .unwrap_or_else(|| soroban_sdk::Vec::new(&env)); + + // Check if oracle with this name already exists + for existing in oracles.iter() { + if existing.name == oracle.name { + return Err(Error::AlreadyInitialized); + } + } + + oracles.push_back(oracle); + env.storage().persistent().set(&DataKey::ExternalOracles, &oracles); + + Ok(()) + } + + /// Remove a trusted external oracle. + /// + /// Only admins can remove external oracles. + pub fn remove_external_oracle(env: Env, admin: Address, oracle_name: Symbol) -> Result<(), Error> { + _require_not_destroyed(&env); + crate::auth::_require_not_frozen(&env); + admin.require_auth(); + crate::auth::_require_authorized(&env, &admin); + + let mut oracles: soroban_sdk::Vec = env + .storage() + .persistent() + .get(&DataKey::ExternalOracles) + .unwrap_or_else(|| soroban_sdk::Vec::new(&env)); + + let mut found = false; + let mut i = 0; + while i < oracles.len() { + if oracles.get(i).unwrap().name == oracle_name { + found = true; + // Remove by swapping with the last element and popping + let last_idx = oracles.len() - 1; + if i != last_idx { + oracles.set(i, oracles.get(last_idx).unwrap()); + } + oracles.pop_back(); + break; + } + i += 1; + } + + if !found { + return Err(Error::AssetNotFound); + } + + env.storage().persistent().set(&DataKey::ExternalOracles, &oracles); + + Ok(()) + } + + /// Get all configured external oracles. + pub fn get_external_oracles(env: Env) -> soroban_sdk::Vec { + env.storage() + .persistent() + .get(&DataKey::ExternalOracles) + .unwrap_or_else(|| soroban_sdk::Vec::new(&env)) + } + + /// Compare internal price with external oracle price. + /// + /// Returns the price comparison data including delta and whether it exceeds threshold. + pub fn compare_with_external(env: Env, asset: Symbol, oracle_name: Symbol) -> Result { + // Get internal price + let internal_price_data = Self::get_price(env.clone(), asset.clone(), true)?; + let internal_price = internal_price_data.price; + + // Find the external oracle + let oracles = Self::get_external_oracles(env.clone()); + let mut external_oracle: Option = None; + for oracle in oracles.iter() { + if oracle.name == oracle_name { + external_oracle = Some(oracle); + break; + } + } + + let oracle = external_oracle.ok_or(Error::AssetNotFound)?; + + // Query external oracle for price + // Assuming external oracle has a similar interface: get_price(asset) -> i128 + // This is a cross-contract call + let external_price: i128 = env.invoke_contract::( + &oracle.address, + &Symbol::new(&env, "get_last_price"), + soroban_sdk::vec![&env, asset.clone().into_val(&env)], + ); + + // Calculate delta + let price_delta = (internal_price - external_price).abs(); + let delta_bps = calculate_percentage_difference_bps(internal_price, external_price) + .unwrap_or(0); + + let exceeds_threshold = delta_bps > oracle.max_delta_bps as i128; + + Ok(crate::types::PriceComparison { + asset, + internal_price, + external_price, + price_delta, + delta_bps, + exceeds_threshold, + }) + } } mod asset_symbol; diff --git a/contracts/price-oracle/src/types.rs b/contracts/price-oracle/src/types.rs index acce871..416dd69 100644 --- a/contracts/price-oracle/src/types.rs +++ b/contracts/price-oracle/src/types.rs @@ -43,6 +43,8 @@ pub enum DataKey { ActionVotes(u64), /// Counter for generating unique action IDs. ActionIdCounter, + /// Trusted external oracle contracts for secondary validation. + ExternalOracles, } /// Represents an asset and its relative weight in an index basket. @@ -246,3 +248,33 @@ pub struct ProposedAction { /// Whether the action has been cancelled. pub cancelled: bool, } + +/// Configuration for an external oracle used for secondary validation. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExternalOracle { + /// Address of the external oracle contract. + pub address: Address, + /// Name/identifier of the oracle (e.g., "Band", "Pyth"). + pub name: Symbol, + /// Maximum allowed delta between internal and external prices (in basis points). + pub max_delta_bps: u32, +} + +/// Result of comparing internal price with external oracle price. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PriceComparison { + /// The asset being compared. + pub asset: Symbol, + /// Internal price from this oracle. + pub internal_price: i128, + /// External price from the trusted oracle. + pub external_price: i128, + /// Absolute difference between prices. + pub price_delta: i128, + /// Percentage difference in basis points. + pub delta_bps: i128, + /// Whether the delta exceeds the configured threshold. + pub exceeds_threshold: bool, +}