diff --git a/Cargo.lock b/Cargo.lock index 729499bd..30e45726 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "ahash" diff --git a/contracts/predictify-hybrid/src/audit_trail.rs b/contracts/predictify-hybrid/src/audit_trail.rs index c8b79b19..5aa09442 100644 --- a/contracts/predictify-hybrid/src/audit_trail.rs +++ b/contracts/predictify-hybrid/src/audit_trail.rs @@ -28,8 +28,9 @@ pub enum AuditAction { FeesWithdrawn, FeeConfigUpdated, - // Oracle & Config Actions + // Token & Oracle Actions OracleConfigUpdated, + TokenVerified, BetLimitsUpdated, // Resolution & Disputes diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index ed3f3ffb..1e84c4f7 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -7114,6 +7114,90 @@ impl PredictifyHybrid { /// # Errors /// /// This entrypoint surfaces contract errors via panic in internal calls. + /// Verify SAC token decimals match declared value (admin only). + /// + /// This function performs a critical security check on SAC tokens to prevent + /// denomination mistakes that have caused real on-chain losses. It verifies that + /// the token's on-chain decimals() value matches what was declared during registration. + /// + /// This can be called: + /// - Automatically during token registration (via add_global_verified/add_event_verified) + /// - Manually by admin as part of periodic audits or security reviews + /// + /// # Parameters + /// + /// * `env` - The Soroban environment for blockchain operations + /// * `admin` - The administrator address (must be authorized) + /// * `token_contract` - Address of the token contract to verify + /// * `declared_decimals` - The decimals value that was declared during registration + /// + /// # Returns + /// + /// Returns `Result<(), Error>` where: + /// - `Ok(())` - Decimals match (token is safe) + /// - `Err(Error::TokenDecimalsMismatch)` - Mismatch detected (token rejected) + /// - `Err(Error::Unauthorized)` - Caller is not admin + /// + /// # Cross-Contract Call + /// + /// This function performs a cross-contract call to the token contract's + /// `decimals()` function using the Soroban token interface. + /// + /// # Security Notes + /// + /// - Verifies via on-chain decimals() call (cannot be spoofed) + /// - Mismatch indicates potential token misconfiguration + /// - Rejected tokens cannot be used for betting/payouts + /// - All registration paths should use verified variants + /// + /// # Example + /// + /// ```rust,ignore + /// let token_contract = Address::from_string("GBUQW..."); + /// PredictifyHybrid::re_verify_token(&env, &admin, &token_contract, 7)?; + /// // Returns Ok if decimals match, TokenDecimalsMismatch error otherwise + /// ``` + /// + /// # Errors + /// + /// Returns [`Error`] when: + /// - `Error::Unauthorized` - Caller is not the contract admin + /// - `Error::TokenDecimalsMismatch` - On-chain decimals don't match declared value + /// - Other errors from cross-contract call or storage operations + /// + /// # Events + /// + /// Emits audit trail record of verification attempt (success or failure). + pub fn re_verify_token( + env: Env, + admin: Address, + token_contract: Address, + declared_decimals: u32, + ) -> Result<(), Error> { + // Verify admin authorization + Self::require_primary_admin(&env, &admin)?; + + // Create temporary asset for verification + let asset = crate::tokens::Asset { + contract: token_contract.clone(), + symbol: Symbol::new(&env, "TEMP"), + decimals: declared_decimals, + }; + + // Perform decimals verification via cross-contract call + crate::tokens::verify_token_decimals(&env, &asset)?; + + // Record verification in audit trail + crate::audit_trail::AuditTrailManager::append_record( + &env, + crate::audit_trail::AuditAction::TokenVerified, + admin.clone(), + Map::new(&env), + ); + + Ok(()) + } + /// /// # Events /// diff --git a/contracts/predictify-hybrid/src/tokens.rs b/contracts/predictify-hybrid/src/tokens.rs index 8153725f..9b680f2b 100644 --- a/contracts/predictify-hybrid/src/tokens.rs +++ b/contracts/predictify-hybrid/src/tokens.rs @@ -213,6 +213,66 @@ impl TokenRegistry { global_assets.iter().any(|a| a == *asset) } + /// Adds an asset to the global allowed registry with decimals verification. + /// + /// This function performs a critical security check by verifying that the + /// declared decimals match the on-chain SAC decimals() value. This prevents + /// denomination mistakes that have caused real losses on other Stellar protocols. + /// + /// # Errors + /// * `Error::TokenDecimalsMismatch` if declared decimals don't match on-chain value. + /// + /// # Security Notes + /// - Performs cross-contract call to token's decimals() function + /// - Rejects registration if mismatch detected + /// - Should only be called by admin + pub fn add_global_verified(env: &Env, asset: &Asset) -> Result<(), Error> { + // Verify decimals before registration + verify_token_decimals(env, asset)?; + + let global_key = Symbol::new(env, "allowed_assets_global"); + let mut global_assets: Vec = env + .storage() + .persistent() + .get(&global_key) + .unwrap_or(Vec::new(env)); + if !global_assets.iter().any(|a| a == *asset) { + global_assets.push_back(asset.clone()); + env.storage().persistent().set(&global_key, &global_assets); + } + Ok(()) + } + + /// Adds an asset to a specific market's allowed registry with decimals verification. + /// + /// # Parameters + /// * `env` - Soroban environment. + /// * `market_id` - Market identifier. + /// * `asset` - The asset to register. + /// + /// # Errors + /// * `Error::TokenDecimalsMismatch` if declared decimals don't match on-chain value. + pub fn add_event_verified(env: &Env, market_id: &Symbol, asset: &Asset) -> Result<(), Error> { + // Verify decimals before registration + verify_token_decimals(env, asset)?; + + let event_key = Symbol::new(env, "allowed_assets_evt"); + let per_event_empty: soroban_sdk::Map> = soroban_sdk::Map::new(env); + let mut per_event: soroban_sdk::Map> = env + .storage() + .persistent() + .get(&event_key) + .unwrap_or(per_event_empty); + let empty_assets: Vec = Vec::new(env); + let mut assets: Vec = per_event.get(market_id.clone()).unwrap_or(empty_assets); + if !assets.iter().any(|a| a == *asset) { + assets.push_back(asset.clone()); + per_event.set(market_id.clone(), assets); + env.storage().persistent().set(&event_key, &per_event); + } + Ok(()) + } + /// Adds an asset to the global allowed registry. pub fn add_global(env: &Env, asset: &Asset) { let global_key = Symbol::new(env, "allowed_assets_global"); @@ -508,6 +568,65 @@ pub fn validate_token_operation( Ok(()) } +// ===== SAC DECIMALS VERIFICATION ===== + +/// Verifies that a token's declared decimals match the on-chain value. +/// +/// This is a critical security check that prevents denomination mistakes. +/// Real-world on-chain losses have occurred on other Stellar protocols when +/// tokens with mismatched decimals were trusted without verification. +/// +/// # Parameters +/// * `env` - Soroban environment. +/// * `asset` - The asset to verify. Uses the declared decimals value. +/// +/// # Returns +/// * `Ok(())` if the declared decimals match the SAC's decimals() output. +/// * `Err(Error::TokenDecimalsMismatch)` if they don't match. +/// +/// # Cross-Contract Call +/// This function performs a cross-contract call to the token contract's +/// `decimals()` function using the Soroban token interface. +/// +/// # Example +/// ```rust,ignore +/// let asset = Asset::new(token_contract, "USDC".into(), 7); +/// verify_token_decimals(&env, &asset)?; // Verifies on-chain +/// ``` +pub fn verify_token_decimals(env: &Env, asset: &Asset) -> Result<(), Error> { + // Create a token client for cross-contract call + let client = token::Client::new(env, &asset.contract); + + // Call the on-chain decimals() function + let on_chain_decimals: u32 = client.decimals(); + + // Compare with declared decimals + if on_chain_decimals != asset.decimals { + return Err(Error::TokenDecimalsMismatch); + } + + Ok(()) +} + +/// Batch verification of multiple assets' decimals. +/// +/// Useful for verifying all globally allowed assets or market-specific assets +/// during initialization or periodic audits. +/// +/// # Parameters +/// * `env` - Soroban environment. +/// * `assets` - Vector of assets to verify. +/// +/// # Returns +/// * `Ok(())` if all assets pass verification. +/// * `Err(Error::TokenDecimalsMismatch)` if any asset fails (first failure only). +pub fn verify_token_decimals_batch(env: &Env, assets: &Vec) -> Result<(), Error> { + for asset in assets.iter() { + verify_token_decimals(env, &asset)?; + } + Ok(()) +} + #[cfg(test)] mod test { use super::*;