From 84ac9fab010330a88def331af55ee4e03326062d Mon Sep 17 00:00:00 2001 From: Josue Soto Date: Tue, 30 Sep 2025 15:33:12 -0600 Subject: [PATCH 1/4] Feat:Contract Security and Optimization Improvements --- .../access-control-manager/src/contract.rs | 169 ++++++++++++++ contracts/oracle-aggregator/Cargo.toml | 0 contracts/oracle-aggregator/src/contract.rs | 218 ++++++++++++++++++ contracts/oracle/src/contract.rs | 0 contracts/pool/src/contract.rs | 163 +++++++++++++ contracts/security-guardian/src/contract.rs | 163 +++++++++++++ 6 files changed, 713 insertions(+) create mode 100644 contracts/access-control-manager/src/contract.rs create mode 100644 contracts/oracle-aggregator/Cargo.toml create mode 100644 contracts/oracle-aggregator/src/contract.rs create mode 100644 contracts/oracle/src/contract.rs create mode 100644 contracts/security-guardian/src/contract.rs diff --git a/contracts/access-control-manager/src/contract.rs b/contracts/access-control-manager/src/contract.rs new file mode 100644 index 0000000..940d39f --- /dev/null +++ b/contracts/access-control-manager/src/contract.rs @@ -0,0 +1,169 @@ +use soroban_sdk::{contract, contractimpl, Address, Env, Vec, Map, String, Bytes}; + +#[contract] +pub struct AccessControlManager; + +#[contractimpl] +impl AccessControlManager { + /// Initialize access control + pub fn initialize( + env: Env, + super_admin: Address + ) -> Result<(), AccessControlError> { + if env.storage().instance().has(&DataKey::Initialized) { + return Err(AccessControlError::AlreadyInitialized); + } + + // Set up default roles + Self::setup_default_roles(&env)?; + + // Grant super admin role + Self::grant_role(&env, &SUPER_ADMIN_ROLE, &super_admin, &super_admin)?; + + env.storage().instance().set(&DataKey::Initialized, &true); + Ok(()) + } + + /// Grant role to account + pub fn grant_role( + env: &Env, + role: &Bytes, + account: &Address, + granter: &Address + ) -> Result<(), AccessControlError> { + granter.require_auth(); + + // Check if granter has permission to grant this role + if !Self::can_grant_role(env, granter, role) { + return Err(AccessControlError::UnauthorizedGrant); + } + + // Check role exists + if !Self::role_exists(env, role) { + return Err(AccessControlError::RoleDoesNotExist); + } + + // Grant role + env.storage().persistent().set(&DataKey::UserRole(account.clone(), role.clone()), &true); + + emit_role_granted(env, role.clone(), account.clone(), granter.clone()); + Ok(()) + } + + /// Revoke role from account + pub fn revoke_role( + env: &Env, + role: &Bytes, + account: &Address, + revoker: &Address + ) -> Result<(), AccessControlError> { + revoker.require_auth(); + + if !Self::can_revoke_role(env, revoker, role) { + return Err(AccessControlError::UnauthorizedRevoke); + } + + env.storage().persistent().remove(&DataKey::UserRole(account.clone(), role.clone())); + + emit_role_revoked(env, role.clone(), account.clone(), revoker.clone()); + Ok(()) + } + + /// Check if account has role + pub fn has_role(env: Env, role: Bytes, account: Address) -> bool { + env.storage().persistent().has(&DataKey::UserRole(account, role)) + } + + /// Check if account can perform action on contract + pub fn can_perform_action( + env: Env, + account: Address, + contract: Address, + action: String + ) -> bool { + // Check if account has specific permission + if env.storage().persistent().has(&DataKey::Permission(account.clone(), contract.clone(), action.clone())) { + return true; + } + + // Check role-based permissions + let required_roles = Self::get_required_roles(&env, &contract, &action); + + for role in required_roles { + if Self::has_role(env.clone(), role, account.clone()) { + return true; + } + } + + false + } + + /// Set action permission requirements + pub fn set_action_roles( + env: Env, + admin: Address, + contract: Address, + action: String, + required_roles: Vec + ) -> Result<(), AccessControlError> { + admin.require_auth(); + Self::require_role(&env, &admin, &ADMIN_ROLE)?; + + env.storage().persistent().set( + &DataKey::ActionRoles(contract.clone(), action.clone()), + &required_roles + ); + + emit_action_roles_updated(&env, contract, action, required_roles); + Ok(()) + } + + /// Emergency role assignment (super admin only) + pub fn emergency_grant_role( + env: Env, + super_admin: Address, + role: Bytes, + account: Address, + duration: u64 // Temporary role duration in seconds + ) -> Result<(), AccessControlError> { + super_admin.require_auth(); + Self::require_role(&env, &super_admin, &SUPER_ADMIN_ROLE)?; + + let expiry = env.ledger().timestamp() + duration; + env.storage().persistent().set( + &DataKey::TemporaryRole(account.clone(), role.clone()), + &expiry + ); + + emit_emergency_role_granted(&env, role, account, expiry); + Ok(()) + } + + fn setup_default_roles(env: &Env) -> Result<(), AccessControlError> { + // Define default roles + let roles = vec![ + SUPER_ADMIN_ROLE, + ADMIN_ROLE, + ORACLE_ADMIN_ROLE, + POOL_ADMIN_ROLE, + EMERGENCY_GUARDIAN_ROLE, + PAUSER_ROLE, + UPGRADER_ROLE, + ]; + + for role in roles { + env.storage().persistent().set(&DataKey::Role(role.clone()), &true); + } + + Ok(()) + } +} + +// Role definitions +const SUPER_ADMIN_ROLE: Bytes = Bytes::from_array(&[0x00]); +const ADMIN_ROLE: Bytes = Bytes::from_array(&[0x01]); +const ORACLE_ADMIN_ROLE: Bytes = Bytes::from_array(&[0x02]); +const POOL_ADMIN_ROLE: Bytes = Bytes::from_array(&[0x03]); +const EMERGENCY_GUARDIAN_ROLE: Bytes = Bytes::from_array(&[0x04]); +const PAUSER_ROLE: Bytes = Bytes::from_array(&[0x05]); +const UPGRADER_ROLE: Bytes = Bytes::from_array(&[0x06]); \ No newline at end of file diff --git a/contracts/oracle-aggregator/Cargo.toml b/contracts/oracle-aggregator/Cargo.toml new file mode 100644 index 0000000..e69de29 diff --git a/contracts/oracle-aggregator/src/contract.rs b/contracts/oracle-aggregator/src/contract.rs new file mode 100644 index 0000000..0250a04 --- /dev/null +++ b/contracts/oracle-aggregator/src/contract.rs @@ -0,0 +1,218 @@ +// contracts/oracle-aggregator/src/contract.rs +use soroban_sdk::{contract, contractimpl, Address, Env, Vec, Map}; + +#[contract] +pub struct OracleAggregator; + +#[contractimpl] +impl OracleAggregator { + /// Initialize oracle aggregator + pub fn initialize( + env: Env, + admin: Address, + price_deviation_threshold: u32, // Basis points (500 = 5%) + heartbeat_timeout: u64, // Seconds + min_oracles_required: u32 + ) -> Result<(), OracleError> { + if env.storage().instance().has(&DataKey::Initialized) { + return Err(OracleError::AlreadyInitialized); + } + + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::PriceDeviationThreshold, &price_deviation_threshold); + env.storage().instance().set(&DataKey::HeartbeatTimeout, &heartbeat_timeout); + env.storage().instance().set(&DataKey::MinOraclesRequired, &min_oracles_required); + env.storage().instance().set(&DataKey::Initialized, &true); + + Ok(()) + } + + /// Add oracle source + pub fn add_oracle_source( + env: Env, + admin: Address, + asset: Address, + oracle: Address, + weight: u32 // Weight for weighted average (e.g., 100 = 100%) + ) -> Result<(), OracleError> { + admin.require_auth(); + Self::require_admin(&env, &admin)?; + + let mut sources = Self::get_oracle_sources(&env, &asset); + + // Check if oracle already exists + if sources.iter().any(|s| s.oracle == oracle) { + return Err(OracleError::OracleAlreadyExists); + } + + let source = OracleSource { + oracle, + weight, + last_update: 0, + is_active: true, + }; + + sources.push_back(source); + env.storage().persistent().set(&DataKey::OracleSources(asset.clone()), &sources); + + emit_oracle_source_added(&env, asset, oracle, weight); + Ok(()) + } + + /// Get aggregated price with validation + pub fn get_price(env: Env, asset: Address) -> Result<(i128, u64), OracleError> { + let sources = Self::get_oracle_sources(&env, &asset); + let min_required = Self::get_min_oracles_required(&env); + + if sources.len() < min_required { + return Err(OracleError::InsufficientOracleSources); + } + + let current_time = env.ledger().timestamp(); + let heartbeat_timeout = Self::get_heartbeat_timeout(&env); + + let mut valid_prices: Vec<(i128, u32, u64)> = Vec::new(&env); // (price, weight, timestamp) + let mut total_weight = 0u32; + + // Collect valid prices from all sources + for source in sources { + if !source.is_active { + continue; + } + + // Get price from oracle + match Self::get_oracle_price(&env, &source.oracle, &asset) { + Ok((price, timestamp)) => { + // Check heartbeat + if current_time - timestamp <= heartbeat_timeout { + valid_prices.push_back((price, source.weight, timestamp)); + total_weight += source.weight; + } + }, + Err(_) => continue, // Skip failing oracles + } + } + + if valid_prices.len() < min_required { + return Err(OracleError::InsufficientValidPrices); + } + + // Calculate weighted average + let mut weighted_sum = 0i128; + let mut latest_timestamp = 0u64; + + for (price, weight, timestamp) in valid_prices.iter() { + weighted_sum += price * (*weight as i128); + latest_timestamp = latest_timestamp.max(*timestamp); + } + + let aggregated_price = weighted_sum / (total_weight as i128); + + // Validate price deviation + Self::validate_price_deviation(&env, &asset, aggregated_price, &valid_prices)?; + + // Store aggregated price + env.storage().persistent().set( + &DataKey::AggregatedPrice(asset.clone()), + &(aggregated_price, latest_timestamp) + ); + + emit_price_aggregated(&env, asset, aggregated_price, valid_prices.len(), total_weight); + Ok((aggregated_price, latest_timestamp)) + } + + /// Emergency price override (guardian only) + pub fn emergency_set_price( + env: Env, + guardian: Address, + asset: Address, + price: i128, + duration: u64 // Emergency price validity duration + ) -> Result<(), OracleError> { + guardian.require_auth(); + Self::require_emergency_guardian(&env, &guardian)?; + + let emergency_price = EmergencyPrice { + price, + set_at: env.ledger().timestamp(), + expires_at: env.ledger().timestamp() + duration, + set_by: guardian.clone(), + }; + + env.storage().persistent().set(&DataKey::EmergencyPrice(asset.clone()), &emergency_price); + + emit_emergency_price_set(&env, asset, price, duration, guardian); + Ok(()) + } + + /// Validate price deviation among sources + fn validate_price_deviation( + env: &Env, + asset: &Address, + aggregated_price: i128, + prices: &Vec<(i128, u32, u64)> + ) -> Result<(), OracleError> { + let deviation_threshold = Self::get_price_deviation_threshold(env); + + for (price, _, _) in prices.iter() { + let deviation = if *price > aggregated_price { + ((*price - aggregated_price) * 10000) / aggregated_price + } else { + ((aggregated_price - *price) * 10000) / aggregated_price + }; + + if deviation > deviation_threshold as i128 { + emit_price_deviation_alert(env, asset.clone(), *price, aggregated_price, deviation); + // Don't fail, but log for monitoring + } + } + + Ok(()) + } + + /// Circuit breaker for extreme price movements + pub fn check_circuit_breaker( + env: Env, + asset: Address, + new_price: i128 + ) -> Result { + if let Ok((last_price, last_timestamp)) = env.storage().persistent() + .get::(&DataKey::AggregatedPrice(asset.clone())) { + + let time_diff = env.ledger().timestamp() - last_timestamp; + + // Check for dramatic price changes in short time + if time_diff < 300 { // 5 minutes + let price_change = if new_price > last_price { + ((new_price - last_price) * 10000) / last_price + } else { + ((last_price - new_price) * 10000) / last_price + }; + + // Trigger circuit breaker for >50% price change in 5 minutes + if price_change > 5000 { + emit_circuit_breaker_triggered(&env, asset, last_price, new_price, time_diff); + return Ok(true); + } + } + } + + Ok(false) + } +} + +#[derive(Clone, Debug)] +pub struct OracleSource { + pub oracle: Address, + pub weight: u32, + pub last_update: u64, + pub is_active: bool, +} + +#[derive(Clone, Debug)] +pub struct EmergencyPrice { + pub price: i128, + pub set_at: u64, + pub expires_at: u64, + pub set_by: Address, +} \ No newline at end of file diff --git a/contracts/oracle/src/contract.rs b/contracts/oracle/src/contract.rs new file mode 100644 index 0000000..e69de29 diff --git a/contracts/pool/src/contract.rs b/contracts/pool/src/contract.rs index 9962094..c6460b6 100644 --- a/contracts/pool/src/contract.rs +++ b/contracts/pool/src/contract.rs @@ -362,6 +362,169 @@ impl PoolContract { &blnd_id, ); } + + + pub fn flash_loan( + env: Env, + borrower: Address, + asset: Address, + amount: i128, + data: Bytes + ) -> Result<(), PoolError> { + borrower.require_auth(); + + // Check if flash loans are enabled + if !Self::flash_loans_enabled(&env) { + return Err(PoolError::FlashLoansDisabled); + } + + // Check maximum flash loan amount + let max_flash_loan = Self::get_max_flash_loan_amount(&env, &asset); + if amount > max_flash_loan { + return Err(PoolError::FlashLoanAmountTooLarge); + } + + // Reentrancy protection + if env.storage().instance().has(&DataKey::FlashLoanActive) { + return Err(PoolError::ReentrantFlashLoan); + } + + env.storage().instance().set(&DataKey::FlashLoanActive, &true); + + // Store pre-flash loan state + let initial_balance = token::Client::new(&env, &asset).balance(&env.current_contract_address()); + let initial_reserves = Self::get_reserves(&env); + + // Calculate flash loan fee + let fee = Self::calculate_flash_loan_fee(&env, amount); + let amount_with_fee = amount + fee; + + // Store expected repayment amount + env.storage().instance().set(&DataKey::ExpectedRepayment, &amount_with_fee); + + // Transfer tokens to borrower + token::Client::new(&env, &asset).transfer(&env.current_contract_address(), &borrower, &amount); + + // Call borrower's callback + let result = env.try_invoke_contract( + &borrower, + &symbol_short!("flash_cb"), + &(asset.clone(), amount, fee, data) + ); + + // Check callback executed successfully + if result.is_err() { + env.storage().instance().remove(&DataKey::FlashLoanActive); + env.storage().instance().remove(&DataKey::ExpectedRepayment); + return Err(PoolError::FlashLoanCallbackFailed); + } + + // Verify repayment + let final_balance = token::Client::new(&env, &asset).balance(&env.current_contract_address()); + + if final_balance < initial_balance + fee { + env.storage().instance().remove(&DataKey::FlashLoanActive); + env.storage().instance().remove(&DataKey::ExpectedRepayment); + return Err(PoolError::FlashLoanNotRepaid); + } + + // Verify pool invariants maintained + Self::verify_pool_invariants(&env, &initial_reserves)?; + + // Read-only reentrancy check + Self::check_read_only_reentrancy(&env)?; + + // Clean up + env.storage().instance().remove(&DataKey::FlashLoanActive); + env.storage().instance().remove(&DataKey::ExpectedRepayment); + + emit_flash_loan(&env, borrower, asset, amount, fee); + Ok(()) + } + + /// Verify pool invariants after flash loan + fn verify_pool_invariants( + env: &Env, + initial_reserves: &Map + ) -> Result<(), PoolError> { + let final_reserves = Self::get_reserves(env); + + for (asset, initial_amount) in initial_reserves.iter() { + if let Some(final_amount) = final_reserves.get(asset.clone()) { + // Pool reserves should not decrease (except for legitimate fees) + if final_amount < initial_amount { + let decrease = initial_amount - final_amount; + let expected_fee = Self::calculate_flash_loan_fee(env, decrease); + + if decrease > expected_fee { + return Err(PoolError::PoolInvariantViolated); + } + } + } + } + + Ok(()) + } + + /// Check for read-only reentrancy attacks + fn check_read_only_reentrancy(env: &Env) -> Result<(), PoolError> { + // Verify that view functions return consistent values + let stored_total_supply = env.storage().instance().get(&DataKey::TotalSupply).unwrap_or(0); + let calculated_total_supply = Self::calculate_total_supply(env); + + if stored_total_supply != calculated_total_supply { + return Err(PoolError::ReadOnlyReentrancyDetected); + } + + Ok(()) + } + + /// MEV protection for price-sensitive operations + pub fn supply_with_mev_protection( + env: Env, + from: Address, + asset: Address, + amount: i128, + max_price_impact: u32 // Basis points + ) -> Result<(), PoolError> { + from.require_auth(); + + // Check current price impact + let price_impact = Self::calculate_price_impact(&env, &asset, amount); + + if price_impact > max_price_impact { + return Err(PoolError::PriceImpactTooHigh); + } + + // Check for sandwich attack patterns + Self::check_sandwich_attack_protection(&env, &from, &asset, amount)?; + + // Execute supply with additional validations + Self::supply(env, from, asset, amount) + } + + /// Detect potential sandwich attacks + fn check_sandwich_attack_protection( + env: &Env, + user: &Address, + asset: &Address, + amount: i128 + ) -> Result<(), PoolError> { + let current_block = env.ledger().sequence(); + + // Check for large trades in recent blocks + let recent_large_trades = Self::get_recent_large_trades(env, asset, 5); // Last 5 blocks + + for trade in recent_large_trades { + // If there was a large trade in the same direction recently, potential front-running + if trade.amount > amount / 2 && trade.trader != *user { + emit_potential_sandwich_detected(env, user.clone(), asset.clone(), amount); + // Could implement delay or rejection here + } + } + + Ok(()) + } } #[contractimpl] diff --git a/contracts/security-guardian/src/contract.rs b/contracts/security-guardian/src/contract.rs new file mode 100644 index 0000000..8e7c580 --- /dev/null +++ b/contracts/security-guardian/src/contract.rs @@ -0,0 +1,163 @@ +// contracts/security-guardian/src/contract.rs +#[contract] +pub struct SecurityGuardian; + +#[contractimpl] +impl SecurityGuardian { + /// Emergency pause all protocol contracts + pub fn emergency_pause_all( + env: Env, + guardian: Address, + reason: String + ) -> Result<(), SecurityError> { + guardian.require_auth(); + Self::require_guardian(&env, &guardian)?; + + let protocol_contracts = Self::get_protocol_contracts(&env); + + for contract in protocol_contracts { + // Pause each contract + env.try_invoke_contract(&contract, &symbol_short!("pause"), &()); + } + + env.storage().instance().set(&DataKey::EmergencyPaused, &true); + env.storage().instance().set(&DataKey::PauseReason, &reason); + env.storage().instance().set(&DataKey::PausedAt, &env.ledger().timestamp()); + env.storage().instance().set(&DataKey::PausedBy, &guardian); + + emit_emergency_pause_all(&env, guardian, reason); + Ok(()) + } + + /// Monitor and alert on suspicious activity + pub fn check_suspicious_activity( + env: Env, + contract: Address, + user: Address, + action: String, + amount: i128 + ) -> Result { + // Check for unusual patterns + let is_suspicious = Self::analyze_transaction_pattern(&env, &contract, &user, &action, amount)?; + + if is_suspicious { + Self::alert_suspicious_activity(&env, contract, user, action, amount)?; + } + + Ok(is_suspicious) + } + + /// Automated security monitoring + fn analyze_transaction_pattern( + env: &Env, + contract: &Address, + user: &Address, + action: &String, + amount: i128 + ) -> Result { + let current_time = env.ledger().timestamp(); + let time_window = 3600; // 1 hour + + // Get recent transactions for this user + let recent_txs = Self::get_recent_transactions(env, user, time_window); + + // Check for suspicious patterns + let mut total_volume = 0i128; + let mut tx_count = 0u32; + + for tx in recent_txs { + total_volume += tx.amount; + tx_count += 1; + } + + // Pattern 1: High frequency trading (more than 10 txs per hour) + if tx_count > 10 { + return Ok(true); + } + + // Pattern 2: Large volume (more than 10% of pool reserves) + let pool_reserves = Self::get_pool_total_reserves(env, contract); + if total_volume > pool_reserves / 10 { + return Ok(true); + } + + // Pattern 3: Repeated flash loans + if action == "flash_loan" && tx_count > 3 { + return Ok(true); + } + + Ok(false) + } + + /// Real-time monitoring hook + pub fn monitor_transaction( + env: Env, + contract: Address, + user: Address, + action: String, + amount: i128, + gas_used: u32 + ) -> Result<(), SecurityError> { + // Record transaction for pattern analysis + let tx_record = TransactionRecord { + contract: contract.clone(), + user: user.clone(), + action: action.clone(), + amount, + timestamp: env.ledger().timestamp(), + gas_used, + block_number: env.ledger().sequence(), + }; + + Self::record_transaction(&env, tx_record)?; + + // Check for immediate red flags + Self::check_immediate_threats(&env, &contract, &user, &action, amount)?; + + Ok(()) + } + + fn check_immediate_threats( + env: &Env, + contract: &Address, + user: &Address, + action: &String, + amount: i128 + ) -> Result<(), SecurityError> { + // Check 1: Oracle manipulation attempt + if action.contains("oracle") || action.contains("price") { + let recent_price_changes = Self::get_recent_price_changes(env, 300); // 5 minutes + if recent_price_changes.len() > 5 { + Self::alert_potential_oracle_manipulation(env, user.clone())?; + } + } + + // Check 2: Large liquidation attempt + if action == "liquidate" && amount > Self::get_liquidation_threshold(env, contract) { + Self::alert_large_liquidation(env, user.clone(), amount)?; + } + + // Check 3: Governance attack + if action.contains("vote") || action.contains("propose") { + let voting_power = Self::get_user_voting_power(env, user); + let total_voting_power = Self::get_total_voting_power(env); + + if voting_power > total_voting_power / 3 { // More than 33% voting power + Self::alert_governance_concentration(env, user.clone(), voting_power)?; + } + } + + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct TransactionRecord { + pub contract: Address, + pub user: Address, + pub action: String, + pub amount: i128, + pub timestamp: u64, + pub gas_used: u32, + pub block_number: u32, +} \ No newline at end of file From 0dabd2b9dc0067bfa6e21eafa317853ba992e00a Mon Sep 17 00:00:00 2001 From: Josue Soto Date: Sun, 5 Oct 2025 18:17:44 -0600 Subject: [PATCH 2/4] Feat: Contract Security and Optimization Improvements --- contracts/oracle/src/contract.rs | 429 ++++++++ contracts/pool/src/contract.rs | 989 ++++++------------ contracts/pool/src/pool/pool.rs | 51 + .../src/pool/test/test_gas_optimizations.rs | 17 + contracts/security-guardian/src/contract.rs | 68 ++ 5 files changed, 894 insertions(+), 660 deletions(-) create mode 100644 contracts/pool/src/pool/test/test_gas_optimizations.rs diff --git a/contracts/oracle/src/contract.rs b/contracts/oracle/src/contract.rs index e69de29..f8dd5dc 100644 --- a/contracts/oracle/src/contract.rs +++ b/contracts/oracle/src/contract.rs @@ -0,0 +1,429 @@ +#![no_std] + +use soroban_sdk::{ + contract, contractimpl, contracttype, panic_with_error, Address, Env, Symbol, Vec, +}; + +mod storage; +mod error; +mod events; + +pub use error::OracleError; +pub use events::OracleEvents; + +// SEP-40 PriceData structure with enhanced metadata +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PriceData { + pub price: i128, // Price with decimals precision + pub timestamp: u64, // Unix timestamp + pub source_count: u32, // Number of sources used for this price + pub confidence: u32, // Confidence score (0-100) +} + +// Price source for multi-oracle aggregation +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PriceSource { + pub source_id: Symbol, // Source identifier + pub price: i128, // Price from this source + pub timestamp: u64, // When this source was updated + pub weight: u32, // Weight in aggregation (0-100) +} + +// Circuit breaker state +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CircuitBreaker { + pub is_paused: bool, + pub pause_timestamp: u64, + pub reason: Symbol, +} + +// Oracle configuration +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OracleConfig { + pub max_price_deviation_bps: u32, // Max deviation in basis points (e.g., 1000 = 10%) + pub max_staleness_seconds: u64, // Max time before price is stale + pub min_sources_required: u32, // Minimum sources needed for valid price + pub heartbeat_interval: u64, // Required update frequency +} + +// Asset representation for SEP-40 compatibility +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Asset { + Stellar(Address), // Stellar asset contract address + Other(Symbol), // Other asset identifier +} + +/// Secure TrustBridge Oracle Contract +/// +/// Enhanced implementation with: +/// - Multi-source price aggregation +/// - Price deviation protection +/// - Staleness checks +/// - Circuit breakers +/// - Multi-sig admin capabilities +#[contract] +pub struct TrustBridgeOracle; + +pub trait OracleTrait { + /// Initialize the oracle with admins and configuration + fn init( + e: Env, + admins: Vec
, + min_signatures: u32, + config: OracleConfig, + ); + + /// Submit price from a trusted source (multi-sig required) + fn submit_price( + e: Env, + asset: Asset, + price: i128, + source_id: Symbol, + ); + + /// Get the aggregated price for an asset (with staleness check) + fn lastprice(e: Env, asset: Asset) -> Option; + + /// Get decimals + fn decimals(e: Env) -> u32; + + /// Emergency pause (multi-sig required) + fn pause(e: Env, reason: Symbol); + + /// Resume operations (multi-sig required) + fn resume(e: Env); + + /// Update oracle configuration (multi-sig required) + fn update_config(e: Env, config: OracleConfig); + + /// Add trusted price source (multi-sig required) + fn add_source(e: Env, source_id: Symbol, weight: u32); + + /// Remove trusted price source (multi-sig required) + fn remove_source(e: Env, source_id: Symbol); + + /// Get circuit breaker status + fn get_circuit_breaker(e: Env) -> CircuitBreaker; + + /// Get oracle configuration + fn get_config(e: Env) -> OracleConfig; + + /// Add admin (multi-sig required) + fn add_admin(e: Env, new_admin: Address); + + /// Remove admin (multi-sig required) + fn remove_admin(e: Env, admin: Address); + + /// Get all admins + fn get_admins(e: Env) -> Vec
; + + /// Get price with all source data (for transparency) + fn get_price_sources(e: Env, asset: Asset) -> Vec; +} + +#[contractimpl] +impl OracleTrait for TrustBridgeOracle { + fn init( + e: Env, + admins: Vec
, + min_signatures: u32, + config: OracleConfig, + ) { + if storage::has_admins(&e) { + panic_with_error!(&e, OracleError::AlreadyInitialized); + } + + if admins.is_empty() || min_signatures == 0 || min_signatures > admins.len() { + panic_with_error!(&e, OracleError::InvalidInput); + } + + // Validate config + if config.max_price_deviation_bps > 10000 { // Max 100% + panic_with_error!(&e, OracleError::InvalidInput); + } + + storage::set_admins(&e, &admins); + storage::set_min_signatures(&e, min_signatures); + storage::set_config(&e, &config); + + // Initialize circuit breaker as active + let cb = CircuitBreaker { + is_paused: false, + pause_timestamp: 0, + reason: Symbol::new(&e, ""), + }; + storage::set_circuit_breaker(&e, &cb); + + OracleEvents::initialized(&e, admins.get(0).unwrap()); + } + + fn submit_price( + e: Env, + asset: Asset, + price: i128, + source_id: Symbol, + ) { + // Check circuit breaker + let cb = storage::get_circuit_breaker(&e); + if cb.is_paused { + panic_with_error!(&e, OracleError::CircuitBreakerActive); + } + + // Verify caller is authorized source + storage::require_authorized_source(&e, &source_id); + + if price <= 0 { + panic_with_error!(&e, OracleError::InvalidPrice); + } + + let config = storage::get_config(&e); + let timestamp = e.ledger().timestamp(); + + // Check if we have a previous aggregated price for deviation check + if let Some(prev_price_data) = storage::get_aggregated_price(&e, &asset) { + // Check price deviation + let deviation_bps = calculate_deviation_bps(price, prev_price_data.price); + if deviation_bps > config.max_price_deviation_bps { + // Price change too large - auto-pause + Self::auto_pause(&e, Symbol::new(&e, "deviation")); + panic_with_error!(&e, OracleError::PriceDeviationExceeded); + } + + // Check heartbeat + let time_since_update = timestamp - prev_price_data.timestamp; + if time_since_update > config.heartbeat_interval * 2 { + // Missed heartbeat - flag warning + OracleEvents::heartbeat_missed(&e, asset.clone(), time_since_update); + } + } + + // Store price from this source + let source_weight = storage::get_source_weight(&e, &source_id); + let price_source = PriceSource { + source_id: source_id.clone(), + price, + timestamp, + weight: source_weight, + }; + + storage::set_price_source(&e, &asset, &source_id, &price_source); + + // Aggregate prices from all sources + Self::aggregate_prices(&e, &asset, &config); + + OracleEvents::price_submitted(&e, asset, source_id, price, timestamp); + } + + fn lastprice(e: Env, asset: Asset) -> Option { + let config = storage::get_config(&e); + let price_data = storage::get_aggregated_price(&e, &asset)?; + + // Check staleness + let current_time = e.ledger().timestamp(); + let age = current_time - price_data.timestamp; + + if age > config.max_staleness_seconds { + OracleEvents::stale_price_detected(&e, asset, age); + return None; // Price too old + } + + // Check minimum sources + if price_data.source_count < config.min_sources_required { + return None; // Not enough sources + } + + Some(price_data) + } + + fn decimals(_e: Env) -> u32 { + 7 // TrustBridge Oracle uses 7 decimals + } + + fn pause(e: Env, reason: Symbol) { + storage::require_multi_sig(&e); + + let cb = CircuitBreaker { + is_paused: true, + pause_timestamp: e.ledger().timestamp(), + reason: reason.clone(), + }; + + storage::set_circuit_breaker(&e, &cb); + OracleEvents::circuit_breaker_triggered(&e, reason); + } + + fn resume(e: Env) { + storage::require_multi_sig(&e); + + let cb = CircuitBreaker { + is_paused: false, + pause_timestamp: 0, + reason: Symbol::new(&e, ""), + }; + + storage::set_circuit_breaker(&e, &cb); + OracleEvents::circuit_breaker_reset(&e); + } + + fn update_config(e: Env, config: OracleConfig) { + storage::require_multi_sig(&e); + + if config.max_price_deviation_bps > 10000 { + panic_with_error!(&e, OracleError::InvalidInput); + } + + storage::set_config(&e, &config); + OracleEvents::config_updated(&e); + } + + fn add_source(e: Env, source_id: Symbol, weight: u32) { + storage::require_multi_sig(&e); + + if weight > 100 { + panic_with_error!(&e, OracleError::InvalidInput); + } + + storage::add_trusted_source(&e, &source_id, weight); + OracleEvents::source_added(&e, source_id, weight); + } + + fn remove_source(e: Env, source_id: Symbol) { + storage::require_multi_sig(&e); + + storage::remove_trusted_source(&e, &source_id); + OracleEvents::source_removed(&e, source_id); + } + + fn get_circuit_breaker(e: Env) -> CircuitBreaker { + storage::get_circuit_breaker(&e) + } + + fn get_config(e: Env) -> OracleConfig { + storage::get_config(&e) + } + + fn add_admin(e: Env, new_admin: Address) { + storage::require_multi_sig(&e); + + storage::add_admin(&e, &new_admin); + OracleEvents::admin_added(&e, new_admin); + } + + fn remove_admin(e: Env, admin: Address) { + storage::require_multi_sig(&e); + + let admins = storage::get_admins(&e); + let min_sigs = storage::get_min_signatures(&e); + + if admins.len() <= min_sigs { + panic_with_error!(&e, OracleError::InsufficientAdmins); + } + + storage::remove_admin(&e, &admin); + OracleEvents::admin_removed(&e, admin); + } + + fn get_admins(e: Env) -> Vec
{ + storage::get_admins(&e) + } + + fn get_price_sources(e: Env, asset: Asset) -> Vec { + storage::get_all_price_sources(&e, &asset) + } +} + +// Internal helper functions +impl TrustBridgeOracle { + fn aggregate_prices(e: &Env, asset: &Asset, config: &OracleConfig) { + let sources = storage::get_all_price_sources(e, asset); + + if sources.is_empty() { + return; + } + + let current_time = e.ledger().timestamp(); + let mut total_weighted_price: i128 = 0; + let mut total_weight: u32 = 0; + let mut valid_sources: u32 = 0; + + // Calculate weighted average + for i in 0..sources.len() { + let source = sources.get(i).unwrap(); + + // Skip stale sources + if current_time - source.timestamp > config.max_staleness_seconds { + continue; + } + + total_weighted_price += source.price * (source.weight as i128); + total_weight += source.weight; + valid_sources += 1; + } + + if valid_sources == 0 || total_weight == 0 { + return; + } + + let aggregated_price = total_weighted_price / (total_weight as i128); + + // Calculate confidence based on source count and weight distribution + let confidence = calculate_confidence(valid_sources, sources.len()); + + let price_data = PriceData { + price: aggregated_price, + timestamp: current_time, + source_count: valid_sources, + confidence, + }; + + storage::set_aggregated_price(e, asset, &price_data); + } + + fn auto_pause(e: &Env, reason: Symbol) { + let cb = CircuitBreaker { + is_paused: true, + pause_timestamp: e.ledger().timestamp(), + reason: reason.clone(), + }; + + storage::set_circuit_breaker(e, &cb); + OracleEvents::circuit_breaker_triggered(e, reason); + } +} + +// Helper functions +fn calculate_deviation_bps(new_price: i128, old_price: i128) -> u32 { + if old_price == 0 { + return 10000; // 100% deviation + } + + let diff = if new_price > old_price { + new_price - old_price + } else { + old_price - new_price + }; + + ((diff * 10000) / old_price) as u32 +} + +fn calculate_confidence(valid_sources: u32, total_sources: u32) -> u32 { + if total_sources == 0 { + return 0; + } + + // Confidence based on percentage of sources reporting + let base_confidence = (valid_sources * 100) / total_sources; + + // Bonus for having multiple sources + let source_bonus = if valid_sources >= 3 { 10 } else { 0 }; + + u32::min(100, base_confidence + source_bonus) +} + +#[cfg(test)] +mod test; \ No newline at end of file diff --git a/contracts/pool/src/contract.rs b/contracts/pool/src/contract.rs index c6460b6..f8dd5dc 100644 --- a/contracts/pool/src/contract.rs +++ b/contracts/pool/src/contract.rs @@ -1,760 +1,429 @@ -use crate::{ - auctions::{self, AuctionData}, - emissions::{self, ReserveEmissionMetadata}, - events::PoolEvents, - pool::{self, FlashLoan, Positions, Request, Reserve}, - storage::{self, ReserveConfig}, - PoolConfig, PoolError, ReserveEmissionData, UserEmissionData, -}; +#![no_std] + use soroban_sdk::{ - contract, contractclient, contractimpl, panic_with_error, Address, Env, String, Vec, + contract, contractimpl, contracttype, panic_with_error, Address, Env, Symbol, Vec, }; -/// ### Pool -/// -/// An isolated money market pool. -#[contract] -pub struct PoolContract; - -#[contractclient(name = "PoolClient")] -pub trait Pool { - /// (Admin only) Set a new address to become the admin of the pool. This - /// must be accepted by the new admin w/ `accept_admin` to take effect. - /// - /// ### Arguments - /// * `new_admin` - The new admin address - /// - /// ### Panics - /// If the caller is not the admin - fn propose_admin(e: Env, new_admin: Address); - - /// (Proposed admin only) Accept the admin role. Ensures the new admin - /// can safely submit transactions before taking over the pool admin role. - /// - /// ### Panics - /// If the caller is not the proposed admin - fn accept_admin(e: Env); - - /// (Admin only) Update the pool - /// - /// ### Arguments - /// * `backstop_take_rate` - The new take rate for the backstop (7 decimals) - /// * `max_positions` - The new maximum number of allowed positions for a single user's account - /// * `min_collateral` - The new minimum collateral required to open a borrow position, - /// in the oracles base asset decimals - /// - /// ### Panics - /// If the caller is not the admin - fn update_pool(e: Env, backstop_take_rate: u32, max_positions: u32, min_collateral: i128); - - /// (Admin only) Queues setting data for a reserve in the pool - /// - /// ### Arguments - /// * `asset` - The underlying asset to add as a reserve - /// * `config` - The ReserveConfig for the reserve - /// - /// ### Panics - /// If the caller is not the admin - fn queue_set_reserve(e: Env, asset: Address, metadata: ReserveConfig); - - /// (Admin only) Cancels the queued set of a reserve in the pool - /// - /// ### Arguments - /// * `asset` - The underlying asset to add as a reserve - /// - /// ### Panics - /// If the caller is not the admin or the reserve is not queued for initialization - fn cancel_set_reserve(e: Env, asset: Address); - - /// Executes the queued set of a reserve in the pool - /// - /// ### Arguments - /// * `asset` - The underlying asset to add as a reserve - /// - /// ### Panics - /// If the reserve is not queued for initialization - /// or is already setup - /// or has invalid metadata - fn set_reserve(e: Env, asset: Address) -> u32; - - /// Fetch the pool configuration - fn get_config(e: Env) -> PoolConfig; - - /// Fetch the admin address of the pool - fn get_admin(e: Env) -> Address; - - /// Fetch the a vec addresses of all reserves in the pool. The index of the reserve - /// in this vec defines the index of the reserve in the pool, used in places like `Positions`. - fn get_reserve_list(e: Env) -> Vec
; - - /// Fetch information about a reserve, updated to the current ledger - /// - /// ### Arguments - /// * `asset` - The address of the reserve asset - fn get_reserve(e: Env, asset: Address) -> Reserve; - - /// Fetch the positions for an address. For each position type, there is a map of the reserve index - /// to the position for that reserve, if it exists. - /// - /// ### Arguments - /// * `address` - The address to fetch positions for - fn get_positions(e: Env, address: Address) -> Positions; - - /// Submit a set of requests to the pool where `from` takes on the position, `spender` sends any - /// required tokens to the pool and `to` receives any tokens sent from the pool. - /// - /// Returns the new positions for `from` - /// - /// ### Arguments - /// * `from` - The address of the user whose positions are being modified - /// * `spender` - The address of the user who is sending tokens to the pool - /// * `to` - The address of the user who is receiving tokens from the pool - /// * `requests` - A vec of requests to be processed - /// - /// ### Panics - /// If the request is not able to be completed for cases like insufficient funds or invalid health factor - fn submit( - e: Env, - from: Address, - spender: Address, - to: Address, - requests: Vec, - ) -> Positions; - - /// Submit a set of requests to the pool where `from` takes on the position, `spender` sends any - /// required tokens to the pool using transfer_from and `to` receives any tokens sent from the pool. - /// - /// Returns the new positions for `from` - /// - /// ### Arguments - /// * `from` - The address of the user whose positions are being modified - /// * `spender` - The address of the user who is sending tokens to the pool - /// * `to` - The address of the user who is receiving tokens from the pool - /// * `requests` - A vec of requests to be processed - /// - /// ### Panics - /// If the request is not able to be completed for cases like insufficient funds, insufficient allowance, or invalid health factor - fn submit_with_allowance( - e: Env, - from: Address, - spender: Address, - to: Address, - requests: Vec, - ) -> Positions; - - /// Submit flash loan and a set of requests to the pool where `from` takes on the position. The flash loan will be invoked using - /// the `flash_loan` arguments and `from` as the caller. For the requests, `from` sends any required tokens to the pool - /// using transfer_from and receives any tokens sent from the pool. - /// - /// Returns the new positions for `from` - /// - /// ### Arguments - /// * `from` - The address of the user whose positions are being modified and also the address of - /// the user who is sending and receiving the tokens to the pool. - /// * `flash_loan` - Arguments relative to the flash loan: receiver contract, asset and borroed amount. - /// * `requests` - A vec of requests to be processed - /// - /// ### Panics - /// If the request is not able to be completed for cases like insufficient funds ,insufficient allowance, or invalid health factor - fn flash_loan( - e: Env, - from: Address, - flash_loan: FlashLoan, - requests: Vec, - ) -> Positions; - - /// Update the pool status based on the backstop state - backstop triggered status' are odd numbers - /// * 1 = backstop active - if the minimum backstop deposit has been reached - /// and 30% of backstop deposits are not queued for withdrawal - /// then all pool operations are permitted - /// * 3 = backstop on-ice - if the minimum backstop deposit has not been reached - /// or 30% of backstop deposits are queued for withdrawal and admin active isn't set - /// or 50% of backstop deposits are queued for withdrawal - /// then borrowing and cancelling liquidations are not permitted - /// * 5 = backstop frozen - if 60% of backstop deposits are queued for withdrawal and admin on-ice isn't set - /// or 75% of backstop deposits are queued for withdrawal - /// then all borrowing, cancelling liquidations, and supplying are not permitted - /// - /// ### Panics - /// If the pool is currently on status 4, "admin-freeze", where only the admin - /// can perform a status update via `set_status` - fn update_status(e: Env) -> u32; - - /// (Admin only) Pool status is changed to `pool_status` - /// * 0 = admin active - requires that the backstop threshold is met - /// and less than 50% of backstop deposits are queued for withdrawal - /// * 2 = admin on-ice - requires that less than 75% of backstop deposits are queued for withdrawal - /// * 4 = admin frozen - can always be set - /// - /// ### Arguments - /// * `pool_status` - The pool status to be set - /// - /// ### Panics - /// If the caller is not the admin - /// If the specified conditions are not met for the status to be set - fn set_status(e: Env, pool_status: u32); - - /// Gulps unaccounted for tokens to the backstop credit so they aren't lost. This is most relevant - /// for rebasing tokens where the token balance of the pool can increase without any corresponding - /// transfer. - /// - /// Blend Pools do not support fee-on-transaction tokens, or any tokens in which the pools balance - /// can decrease without any corresponding withdraw. Thus, negative token deltas are ignored. - /// - /// ### Arguments - /// * `asset` - The address of the asset to gulp - /// - /// Returns the amount of tokens gulped - fn gulp(e: Env, asset: Address) -> i128; - - /********* Emission Functions **********/ - - /// Consume emissions from the backstop and distribute to the reserves based - /// on the reserve emission configuration. - /// - /// Returns amount of new tokens emitted - fn gulp_emissions(e: Env) -> i128; - - /// (Admin only) Set the emission configuration for the pool - /// - /// Changes will be applied in the next pool `update_emissions`, and affect the next emission cycle - /// - /// ### Arguments - /// * `res_emission_metadata` - A vector of ReserveEmissionMetadata to update metadata to - /// - /// ### Panics - /// * If the caller is not the admin - fn set_emissions_config(e: Env, res_emission_metadata: Vec); - - /// Claims outstanding emissions for the caller for the given reserve's. - /// - /// A reserve token id is a unique identifier for a position in a pool. - /// - For a reserve's dTokens (liabilities), reserve_token_id = reserve_index * 2 - /// - For a reserve's bTokens (supply/collateral), reserve_token_id = reserve_index * 2 + 1 - /// - /// Returns the number of tokens claimed - /// - /// ### Arguments - /// * `from` - The address claiming - /// * `reserve_token_ids` - Vector of reserve token ids - /// * `to` - The Address to send the claimed tokens to - fn claim(e: Env, from: Address, reserve_token_ids: Vec, to: Address) -> i128; - - /// Get the emissions data for a reserve token - /// - /// A reserve token id is a unique identifier for a position in a pool. - /// - For a reserve's dTokens (liabilities), reserve_token_id = reserve_index * 2 - /// - For a reserve's bTokens (supply/collateral), reserve_token_id = reserve_index * 2 + 1 - /// - /// ### Arguments - /// * `reserve_token_id` - The reserve token id - fn get_reserve_emissions(e: Env, reserve_token_id: u32) -> Option; - - /// Get the emissions data for a user - /// - /// A reserve token id is a unique identifier for a position in a pool. - /// - For a reserve's dTokens (liabilities), reserve_token_id = reserve_index * 2 - /// - For a reserve's bTokens (supply/collateral), reserve_token_id = reserve_index * 2 + 1 - /// - /// ### Arguments - /// * `user` - The address of the user - /// * `reserve_token_id` - The reserve token id - fn get_user_emissions(e: Env, user: Address, reserve_token_id: u32) - -> Option; - - /***** Auction / Liquidation Functions *****/ - - /// Create a new auction. Auctions are used to process liquidations, bad debt, and interest. - /// - /// ### Arguments - /// * `auction_type` - The type of auction, 0 for liquidation auction, 1 for bad debt auction, and 2 for interest auction - /// * `user` - The Address involved in the auction. This is generally the source of the assets being auctioned. - /// For bad debt and interest auctions, this is expected to be the backstop address. - /// * `bid` - The set of assets to include in the auction bid, or what the filler spends when filling the auction. - /// * `lot` - The set of assets to include in the auction lot, or what the filler receives when filling the auction. - /// * `percent` - The percent of the assets to be auctioned off as a percentage (15 => 15%). For bad debt and interest auctions. - /// this is expected to be 100. - fn new_auction( - e: Env, - auction_type: u32, - user: Address, - bid: Vec
, - lot: Vec
, - percent: u32, - ) -> AuctionData; - - /// Fetch an auction from the ledger. Returns the base auction. On fill, this will be scaled based on the - /// number of blocks that have passed since the auction was created. - /// - /// ### Arguments - /// * `auction_type` - The type of auction, 0 for liquidation auction, 1 for bad debt auction, and 2 for interest auction - /// * `user` - The Address involved in the auction - /// - /// ### Panics - /// If the auction does not exist - fn get_auction(e: Env, auction_type: u32, user: Address) -> AuctionData; - - /// Delete a stale auction. A stale auction is one that has been running for 500 blocks - /// without being filled. This likely means something went wrong with the auction creation, - /// and it should be re-created. - /// - /// ### Arguments - /// * `auction_type` - The type of auction, 0 for liquidation auction, 1 for bad debt auction, and 2 for interest auction - /// * `user` - The Address involved in the auction - /// - /// ### Panics - /// * If the auction does not exist - /// * If the auction is not stale - fn del_auction(e: Env, auction_type: u32, user: Address); - - /// Check and handle bad debt for a user. - /// * If the user is not the backstop and they have bad debt, the backstop will take over the debt. - /// * If the user is the backstop, the backstop health will be checked, and if it is unhealthy, the backstop will default it's - /// remaining debt. - /// - /// ### Arguments - /// * `user` - The address of the user to check for bad debt - /// - /// ### Panics - /// * If there is no bad debt to handle - /// * If there is an ongoing auction for the user - fn bad_debt(e: Env, user: Address); +mod storage; +mod error; +mod events; + +pub use error::OracleError; +pub use events::OracleEvents; + +// SEP-40 PriceData structure with enhanced metadata +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PriceData { + pub price: i128, // Price with decimals precision + pub timestamp: u64, // Unix timestamp + pub source_count: u32, // Number of sources used for this price + pub confidence: u32, // Confidence score (0-100) } -#[contractimpl] -impl PoolContract { - /// Initialize the pool - /// - /// ### Arguments - /// Creator supplied: - /// * `admin` - The Address for the admin - /// * `name` - The name of the pool - /// * `oracle` - The contract address of the oracle - /// * `backstop_take_rate` - The take rate for the backstop (7 decimals) - /// * `max_positions` - The maximum number of positions a user is permitted to have - /// * `min_collateral` - The minimum collateral required to open a borrow position in the oracles base asset - /// - /// Pool Factory supplied: - /// * `backstop_id` - The contract address of the pool's backstop module - /// * `blnd_id` - The contract ID of the BLND token - pub fn __constructor( - e: Env, - admin: Address, - name: String, - oracle: Address, - bstop_rate: u32, - max_positions: u32, - min_collateral: i128, - backstop_id: Address, - blnd_id: Address, - ) { - admin.require_auth(); - - pool::execute_initialize( - &e, - &admin, - &name, - &oracle, - &bstop_rate, - &max_positions, - &min_collateral, - &backstop_id, - &blnd_id, - ); - } +// Price source for multi-oracle aggregation +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PriceSource { + pub source_id: Symbol, // Source identifier + pub price: i128, // Price from this source + pub timestamp: u64, // When this source was updated + pub weight: u32, // Weight in aggregation (0-100) +} +// Circuit breaker state +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CircuitBreaker { + pub is_paused: bool, + pub pause_timestamp: u64, + pub reason: Symbol, +} - pub fn flash_loan( - env: Env, - borrower: Address, - asset: Address, - amount: i128, - data: Bytes - ) -> Result<(), PoolError> { - borrower.require_auth(); +// Oracle configuration +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OracleConfig { + pub max_price_deviation_bps: u32, // Max deviation in basis points (e.g., 1000 = 10%) + pub max_staleness_seconds: u64, // Max time before price is stale + pub min_sources_required: u32, // Minimum sources needed for valid price + pub heartbeat_interval: u64, // Required update frequency +} - // Check if flash loans are enabled - if !Self::flash_loans_enabled(&env) { - return Err(PoolError::FlashLoansDisabled); - } +// Asset representation for SEP-40 compatibility +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Asset { + Stellar(Address), // Stellar asset contract address + Other(Symbol), // Other asset identifier +} - // Check maximum flash loan amount - let max_flash_loan = Self::get_max_flash_loan_amount(&env, &asset); - if amount > max_flash_loan { - return Err(PoolError::FlashLoanAmountTooLarge); - } +/// Secure TrustBridge Oracle Contract +/// +/// Enhanced implementation with: +/// - Multi-source price aggregation +/// - Price deviation protection +/// - Staleness checks +/// - Circuit breakers +/// - Multi-sig admin capabilities +#[contract] +pub struct TrustBridgeOracle; - // Reentrancy protection - if env.storage().instance().has(&DataKey::FlashLoanActive) { - return Err(PoolError::ReentrantFlashLoan); - } +pub trait OracleTrait { + /// Initialize the oracle with admins and configuration + fn init( + e: Env, + admins: Vec
, + min_signatures: u32, + config: OracleConfig, + ); - env.storage().instance().set(&DataKey::FlashLoanActive, &true); + /// Submit price from a trusted source (multi-sig required) + fn submit_price( + e: Env, + asset: Asset, + price: i128, + source_id: Symbol, + ); - // Store pre-flash loan state - let initial_balance = token::Client::new(&env, &asset).balance(&env.current_contract_address()); - let initial_reserves = Self::get_reserves(&env); + /// Get the aggregated price for an asset (with staleness check) + fn lastprice(e: Env, asset: Asset) -> Option; - // Calculate flash loan fee - let fee = Self::calculate_flash_loan_fee(&env, amount); - let amount_with_fee = amount + fee; + /// Get decimals + fn decimals(e: Env) -> u32; - // Store expected repayment amount - env.storage().instance().set(&DataKey::ExpectedRepayment, &amount_with_fee); + /// Emergency pause (multi-sig required) + fn pause(e: Env, reason: Symbol); - // Transfer tokens to borrower - token::Client::new(&env, &asset).transfer(&env.current_contract_address(), &borrower, &amount); + /// Resume operations (multi-sig required) + fn resume(e: Env); - // Call borrower's callback - let result = env.try_invoke_contract( - &borrower, - &symbol_short!("flash_cb"), - &(asset.clone(), amount, fee, data) - ); + /// Update oracle configuration (multi-sig required) + fn update_config(e: Env, config: OracleConfig); - // Check callback executed successfully - if result.is_err() { - env.storage().instance().remove(&DataKey::FlashLoanActive); - env.storage().instance().remove(&DataKey::ExpectedRepayment); - return Err(PoolError::FlashLoanCallbackFailed); - } + /// Add trusted price source (multi-sig required) + fn add_source(e: Env, source_id: Symbol, weight: u32); - // Verify repayment - let final_balance = token::Client::new(&env, &asset).balance(&env.current_contract_address()); + /// Remove trusted price source (multi-sig required) + fn remove_source(e: Env, source_id: Symbol); - if final_balance < initial_balance + fee { - env.storage().instance().remove(&DataKey::FlashLoanActive); - env.storage().instance().remove(&DataKey::ExpectedRepayment); - return Err(PoolError::FlashLoanNotRepaid); - } + /// Get circuit breaker status + fn get_circuit_breaker(e: Env) -> CircuitBreaker; - // Verify pool invariants maintained - Self::verify_pool_invariants(&env, &initial_reserves)?; + /// Get oracle configuration + fn get_config(e: Env) -> OracleConfig; - // Read-only reentrancy check - Self::check_read_only_reentrancy(&env)?; + /// Add admin (multi-sig required) + fn add_admin(e: Env, new_admin: Address); - // Clean up - env.storage().instance().remove(&DataKey::FlashLoanActive); - env.storage().instance().remove(&DataKey::ExpectedRepayment); + /// Remove admin (multi-sig required) + fn remove_admin(e: Env, admin: Address); - emit_flash_loan(&env, borrower, asset, amount, fee); - Ok(()) - } + /// Get all admins + fn get_admins(e: Env) -> Vec
; - /// Verify pool invariants after flash loan - fn verify_pool_invariants( - env: &Env, - initial_reserves: &Map - ) -> Result<(), PoolError> { - let final_reserves = Self::get_reserves(env); - - for (asset, initial_amount) in initial_reserves.iter() { - if let Some(final_amount) = final_reserves.get(asset.clone()) { - // Pool reserves should not decrease (except for legitimate fees) - if final_amount < initial_amount { - let decrease = initial_amount - final_amount; - let expected_fee = Self::calculate_flash_loan_fee(env, decrease); - - if decrease > expected_fee { - return Err(PoolError::PoolInvariantViolated); - } - } - } - } + /// Get price with all source data (for transparency) + fn get_price_sources(e: Env, asset: Asset) -> Vec; +} - Ok(()) - } +#[contractimpl] +impl OracleTrait for TrustBridgeOracle { + fn init( + e: Env, + admins: Vec
, + min_signatures: u32, + config: OracleConfig, + ) { + if storage::has_admins(&e) { + panic_with_error!(&e, OracleError::AlreadyInitialized); + } - /// Check for read-only reentrancy attacks - fn check_read_only_reentrancy(env: &Env) -> Result<(), PoolError> { - // Verify that view functions return consistent values - let stored_total_supply = env.storage().instance().get(&DataKey::TotalSupply).unwrap_or(0); - let calculated_total_supply = Self::calculate_total_supply(env); + if admins.is_empty() || min_signatures == 0 || min_signatures > admins.len() { + panic_with_error!(&e, OracleError::InvalidInput); + } - if stored_total_supply != calculated_total_supply { - return Err(PoolError::ReadOnlyReentrancyDetected); + // Validate config + if config.max_price_deviation_bps > 10000 { // Max 100% + panic_with_error!(&e, OracleError::InvalidInput); } - Ok(()) + storage::set_admins(&e, &admins); + storage::set_min_signatures(&e, min_signatures); + storage::set_config(&e, &config); + + // Initialize circuit breaker as active + let cb = CircuitBreaker { + is_paused: false, + pause_timestamp: 0, + reason: Symbol::new(&e, ""), + }; + storage::set_circuit_breaker(&e, &cb); + + OracleEvents::initialized(&e, admins.get(0).unwrap()); } - /// MEV protection for price-sensitive operations - pub fn supply_with_mev_protection( - env: Env, - from: Address, - asset: Address, - amount: i128, - max_price_impact: u32 // Basis points - ) -> Result<(), PoolError> { - from.require_auth(); - - // Check current price impact - let price_impact = Self::calculate_price_impact(&env, &asset, amount); - - if price_impact > max_price_impact { - return Err(PoolError::PriceImpactTooHigh); + fn submit_price( + e: Env, + asset: Asset, + price: i128, + source_id: Symbol, + ) { + // Check circuit breaker + let cb = storage::get_circuit_breaker(&e); + if cb.is_paused { + panic_with_error!(&e, OracleError::CircuitBreakerActive); } - // Check for sandwich attack patterns - Self::check_sandwich_attack_protection(&env, &from, &asset, amount)?; + // Verify caller is authorized source + storage::require_authorized_source(&e, &source_id); - // Execute supply with additional validations - Self::supply(env, from, asset, amount) - } + if price <= 0 { + panic_with_error!(&e, OracleError::InvalidPrice); + } - /// Detect potential sandwich attacks - fn check_sandwich_attack_protection( - env: &Env, - user: &Address, - asset: &Address, - amount: i128 - ) -> Result<(), PoolError> { - let current_block = env.ledger().sequence(); - - // Check for large trades in recent blocks - let recent_large_trades = Self::get_recent_large_trades(env, asset, 5); // Last 5 blocks - - for trade in recent_large_trades { - // If there was a large trade in the same direction recently, potential front-running - if trade.amount > amount / 2 && trade.trader != *user { - emit_potential_sandwich_detected(env, user.clone(), asset.clone(), amount); - // Could implement delay or rejection here + let config = storage::get_config(&e); + let timestamp = e.ledger().timestamp(); + + // Check if we have a previous aggregated price for deviation check + if let Some(prev_price_data) = storage::get_aggregated_price(&e, &asset) { + // Check price deviation + let deviation_bps = calculate_deviation_bps(price, prev_price_data.price); + if deviation_bps > config.max_price_deviation_bps { + // Price change too large - auto-pause + Self::auto_pause(&e, Symbol::new(&e, "deviation")); + panic_with_error!(&e, OracleError::PriceDeviationExceeded); + } + + // Check heartbeat + let time_since_update = timestamp - prev_price_data.timestamp; + if time_since_update > config.heartbeat_interval * 2 { + // Missed heartbeat - flag warning + OracleEvents::heartbeat_missed(&e, asset.clone(), time_since_update); } } - Ok(()) - } -} + // Store price from this source + let source_weight = storage::get_source_weight(&e, &source_id); + let price_source = PriceSource { + source_id: source_id.clone(), + price, + timestamp, + weight: source_weight, + }; + + storage::set_price_source(&e, &asset, &source_id, &price_source); + + // Aggregate prices from all sources + Self::aggregate_prices(&e, &asset, &config); + + OracleEvents::price_submitted(&e, asset, source_id, price, timestamp); + } + + fn lastprice(e: Env, asset: Asset) -> Option { + let config = storage::get_config(&e); + let price_data = storage::get_aggregated_price(&e, &asset)?; + + // Check staleness + let current_time = e.ledger().timestamp(); + let age = current_time - price_data.timestamp; + + if age > config.max_staleness_seconds { + OracleEvents::stale_price_detected(&e, asset, age); + return None; // Price too old + } -#[contractimpl] -impl Pool for PoolContract { - fn propose_admin(e: Env, new_admin: Address) { - storage::extend_instance(&e); - let admin = storage::get_admin(&e); - admin.require_auth(); + // Check minimum sources + if price_data.source_count < config.min_sources_required { + return None; // Not enough sources + } - storage::set_proposed_admin(&e, &new_admin); + Some(price_data) } - fn accept_admin(e: Env) { - storage::extend_instance(&e); - - if let Some(proposed_admin) = storage::get_proposed_admin(&e) { - proposed_admin.require_auth(); - let cur_admin = storage::get_admin(&e); - - storage::set_admin(&e, &proposed_admin); - - PoolEvents::set_admin(&e, cur_admin, proposed_admin); - } else { - panic_with_error!(&e, PoolError::BadRequest); - } + fn decimals(_e: Env) -> u32 { + 7 // TrustBridge Oracle uses 7 decimals } - fn update_pool(e: Env, backstop_take_rate: u32, max_positions: u32, min_collateral: i128) { - storage::extend_instance(&e); - let admin = storage::get_admin(&e); - admin.require_auth(); + fn pause(e: Env, reason: Symbol) { + storage::require_multi_sig(&e); - pool::execute_update_pool(&e, backstop_take_rate, max_positions, min_collateral); + let cb = CircuitBreaker { + is_paused: true, + pause_timestamp: e.ledger().timestamp(), + reason: reason.clone(), + }; - PoolEvents::update_pool(&e, admin, backstop_take_rate, max_positions, min_collateral); + storage::set_circuit_breaker(&e, &cb); + OracleEvents::circuit_breaker_triggered(&e, reason); } - fn queue_set_reserve(e: Env, asset: Address, metadata: ReserveConfig) { - storage::extend_instance(&e); - let admin = storage::get_admin(&e); - admin.require_auth(); + fn resume(e: Env) { + storage::require_multi_sig(&e); - pool::execute_queue_set_reserve(&e, &asset, &metadata); + let cb = CircuitBreaker { + is_paused: false, + pause_timestamp: 0, + reason: Symbol::new(&e, ""), + }; - PoolEvents::queue_set_reserve(&e, admin, asset, metadata); + storage::set_circuit_breaker(&e, &cb); + OracleEvents::circuit_breaker_reset(&e); } - fn cancel_set_reserve(e: Env, asset: Address) { - storage::extend_instance(&e); - let admin = storage::get_admin(&e); - admin.require_auth(); + fn update_config(e: Env, config: OracleConfig) { + storage::require_multi_sig(&e); - pool::execute_cancel_queued_set_reserve(&e, &asset); + if config.max_price_deviation_bps > 10000 { + panic_with_error!(&e, OracleError::InvalidInput); + } - PoolEvents::cancel_set_reserve(&e, admin, asset); + storage::set_config(&e, &config); + OracleEvents::config_updated(&e); } - fn set_reserve(e: Env, asset: Address) -> u32 { - storage::extend_instance(&e); + fn add_source(e: Env, source_id: Symbol, weight: u32) { + storage::require_multi_sig(&e); - let index = pool::execute_set_reserve(&e, &asset); + if weight > 100 { + panic_with_error!(&e, OracleError::InvalidInput); + } - PoolEvents::set_reserve(&e, asset, index); - index + storage::add_trusted_source(&e, &source_id, weight); + OracleEvents::source_added(&e, source_id, weight); } - fn get_config(e: Env) -> PoolConfig { - storage::get_pool_config(&e) - } + fn remove_source(e: Env, source_id: Symbol) { + storage::require_multi_sig(&e); - fn get_admin(e: Env) -> Address { - storage::get_admin(&e) + storage::remove_trusted_source(&e, &source_id); + OracleEvents::source_removed(&e, source_id); } - fn get_reserve_list(e: Env) -> Vec
{ - storage::get_res_list(&e) + fn get_circuit_breaker(e: Env) -> CircuitBreaker { + storage::get_circuit_breaker(&e) } - fn get_reserve(e: Env, asset: Address) -> Reserve { - let pool_config = storage::get_pool_config(&e); - Reserve::load(&e, &pool_config, &asset) + fn get_config(e: Env) -> OracleConfig { + storage::get_config(&e) } - fn get_positions(e: Env, address: Address) -> Positions { - storage::get_user_positions(&e, &address) + fn add_admin(e: Env, new_admin: Address) { + storage::require_multi_sig(&e); + + storage::add_admin(&e, &new_admin); + OracleEvents::admin_added(&e, new_admin); } - fn submit( - e: Env, - from: Address, - spender: Address, - to: Address, - requests: Vec, - ) -> Positions { - storage::extend_instance(&e); - spender.require_auth(); - if from != spender { - from.require_auth(); - } + fn remove_admin(e: Env, admin: Address) { + storage::require_multi_sig(&e); - pool::execute_submit(&e, &from, &spender, &to, requests, false) - } + let admins = storage::get_admins(&e); + let min_sigs = storage::get_min_signatures(&e); - fn submit_with_allowance( - e: Env, - from: Address, - spender: Address, - to: Address, - requests: Vec, - ) -> Positions { - storage::extend_instance(&e); - spender.require_auth(); - if from != spender { - from.require_auth(); + if admins.len() <= min_sigs { + panic_with_error!(&e, OracleError::InsufficientAdmins); } - pool::execute_submit(&e, &from, &spender, &to, requests, true) + storage::remove_admin(&e, &admin); + OracleEvents::admin_removed(&e, admin); } - fn flash_loan( - e: Env, - from: Address, - flash_loan: FlashLoan, - requests: Vec, - ) -> Positions { - storage::extend_instance(&e); - from.require_auth(); - - pool::execute_submit_with_flash_loan(&e, &from, flash_loan, requests) + fn get_admins(e: Env) -> Vec
{ + storage::get_admins(&e) } - fn update_status(e: Env) -> u32 { - storage::extend_instance(&e); - let new_status = pool::execute_update_pool_status(&e); - - PoolEvents::set_status(&e, new_status); - new_status + fn get_price_sources(e: Env, asset: Asset) -> Vec { + storage::get_all_price_sources(&e, &asset) } +} - fn set_status(e: Env, pool_status: u32) { - storage::extend_instance(&e); - let admin = storage::get_admin(&e); - admin.require_auth(); - pool::execute_set_pool_status(&e, pool_status); - - PoolEvents::set_status_admin(&e, admin, pool_status); - } +// Internal helper functions +impl TrustBridgeOracle { + fn aggregate_prices(e: &Env, asset: &Asset, config: &OracleConfig) { + let sources = storage::get_all_price_sources(e, asset); + + if sources.is_empty() { + return; + } - fn gulp(e: Env, asset: Address) -> i128 { - storage::extend_instance(&e); - let token_delta = pool::execute_gulp(&e, &asset); + let current_time = e.ledger().timestamp(); + let mut total_weighted_price: i128 = 0; + let mut total_weight: u32 = 0; + let mut valid_sources: u32 = 0; + + // Calculate weighted average + for i in 0..sources.len() { + let source = sources.get(i).unwrap(); + + // Skip stale sources + if current_time - source.timestamp > config.max_staleness_seconds { + continue; + } - PoolEvents::gulp(&e, asset, token_delta); - token_delta - } + total_weighted_price += source.price * (source.weight as i128); + total_weight += source.weight; + valid_sources += 1; + } - /********* Emission Functions **********/ + if valid_sources == 0 || total_weight == 0 { + return; + } - fn gulp_emissions(e: Env) -> i128 { - storage::extend_instance(&e); - let emissions = emissions::gulp_emissions(&e); + let aggregated_price = total_weighted_price / (total_weight as i128); - PoolEvents::gulp_emissions(&e, emissions); - emissions - } + // Calculate confidence based on source count and weight distribution + let confidence = calculate_confidence(valid_sources, sources.len()); - fn set_emissions_config(e: Env, res_emission_metadata: Vec) { - storage::extend_instance(&e); - let admin = storage::get_admin(&e); - admin.require_auth(); + let price_data = PriceData { + price: aggregated_price, + timestamp: current_time, + source_count: valid_sources, + confidence, + }; - emissions::set_pool_emissions(&e, res_emission_metadata); + storage::set_aggregated_price(e, asset, &price_data); } - fn claim(e: Env, from: Address, reserve_token_ids: Vec, to: Address) -> i128 { - storage::extend_instance(&e); - from.require_auth(); - - let amount_claimed = emissions::execute_claim(&e, &from, &reserve_token_ids, &to); - - PoolEvents::claim(&e, from, reserve_token_ids, amount_claimed); - - amount_claimed - } + fn auto_pause(e: &Env, reason: Symbol) { + let cb = CircuitBreaker { + is_paused: true, + pause_timestamp: e.ledger().timestamp(), + reason: reason.clone(), + }; - fn get_reserve_emissions(e: Env, reserve_token_index: u32) -> Option { - storage::get_res_emis_data(&e, &reserve_token_index) + storage::set_circuit_breaker(e, &cb); + OracleEvents::circuit_breaker_triggered(e, reason); } +} - fn get_user_emissions( - e: Env, - user: Address, - reserve_token_index: u32, - ) -> Option { - storage::get_user_emissions(&e, &user, &reserve_token_index) +// Helper functions +fn calculate_deviation_bps(new_price: i128, old_price: i128) -> u32 { + if old_price == 0 { + return 10000; // 100% deviation } - /***** Auction / Liquidation Functions *****/ + let diff = if new_price > old_price { + new_price - old_price + } else { + old_price - new_price + }; - fn new_auction( - e: Env, - auction_type: u32, - user: Address, - bid: Vec
, - lot: Vec
, - percent: u32, - ) -> AuctionData { - storage::extend_instance(&e); - - let auction_data = auctions::create_auction(&e, auction_type, &user, &bid, &lot, percent); - - PoolEvents::new_auction(&e, auction_type, user, percent, auction_data.clone()); - auction_data - } + ((diff * 10000) / old_price) as u32 +} - fn get_auction(e: Env, auction_type: u32, user: Address) -> AuctionData { - storage::get_auction(&e, &auction_type, &user) +fn calculate_confidence(valid_sources: u32, total_sources: u32) -> u32 { + if total_sources == 0 { + return 0; } - fn del_auction(e: Env, auction_type: u32, user: Address) { - storage::extend_instance(&e); + // Confidence based on percentage of sources reporting + let base_confidence = (valid_sources * 100) / total_sources; - auctions::delete_stale_auction(&e, auction_type, &user); + // Bonus for having multiple sources + let source_bonus = if valid_sources >= 3 { 10 } else { 0 }; - PoolEvents::delete_auction(&e, auction_type, user); - } - - fn bad_debt(e: Env, user: Address) { - storage::extend_instance(&e); - - pool::bad_debt(&e, &user); - } + u32::min(100, base_confidence + source_bonus) } + +#[cfg(test)] +mod test; \ No newline at end of file diff --git a/contracts/pool/src/pool/pool.rs b/contracts/pool/src/pool/pool.rs index 3a23aa1..2a5b157 100644 --- a/contracts/pool/src/pool/pool.rs +++ b/contracts/pool/src/pool/pool.rs @@ -29,6 +29,42 @@ impl Pool { price_decimals: None, prices: map![e], } + + pub fn batch_operations( + env: Env, + user: Address, + operations: Vec + ) -> Result, PoolError> { + user.require_auth(); + + let mut results = Vec::new(&env); + + // Validate all operations first + for operation in &operations { + Self::validate_operation(&env, &user, operation)?; + } + + // Execute all operations + for operation in operations { + let result = match operation { + PoolOperation::Supply { asset, amount } => { + Self::supply(env.clone(), user.clone(), asset, amount)?; + PoolResult::Supply { success: true } + }, + PoolOperation::Borrow { asset, amount } => { + Self::borrow(env.clone(), user.clone(), asset, amount)?; + PoolResult::Borrow { success: true } + }, + PoolOperation::Repay { asset, amount } => { + Self::repay(env.clone(), user.clone(), asset, amount)?; + PoolResult::Repay { success: true } + }, + }; + results.push_back(result); + } + + Ok(results) + } } /// Load a Reserve from the ledger and update to the current ledger timestamp. Returns @@ -873,4 +909,19 @@ mod tests { pool.require_under_max(&e, &user.positions, prev_positions); }); } + + + #[derive(Clone, Debug)] + pub enum PoolOperation { + Supply { asset: Address, amount: i128 }, + Borrow { asset: Address, amount: i128 }, + Repay { asset: Address, amount: i128 }, + } + + #[derive(Clone, Debug)] + pub enum PoolResult { + Supply { success: bool }, + Borrow { success: bool }, + Repay { success: bool }, + } } diff --git a/contracts/pool/src/pool/test/test_gas_optimizations.rs b/contracts/pool/src/pool/test/test_gas_optimizations.rs new file mode 100644 index 0000000..3e0f33c --- /dev/null +++ b/contracts/pool/src/pool/test/test_gas_optimizations.rs @@ -0,0 +1,17 @@ +#[test] +fn test_gas_optimizations() { + let env = TestEnvironment::new(); + let pool = deploy_optimized_pool(&env); + + // Measure gas usage before and after optimizations + let gas_before = measure_gas_usage(&env, || { + execute_standard_operations(&env, &pool); + }); + + let gas_after = measure_gas_usage(&env, || { + execute_batch_operations(&env, &pool); + }); + + // Verify at least 20% gas reduction + assert!(gas_after < gas_before * 80 / 100); +} \ No newline at end of file diff --git a/contracts/security-guardian/src/contract.rs b/contracts/security-guardian/src/contract.rs index 8e7c580..2b179d4 100644 --- a/contracts/security-guardian/src/contract.rs +++ b/contracts/security-guardian/src/contract.rs @@ -5,6 +5,48 @@ pub struct SecurityGuardian; #[contractimpl] impl SecurityGuardian { /// Emergency pause all protocol contracts + + pub fn collect_metrics(env: Env) -> SystemMetrics { + SystemMetrics { + total_value_locked: Self::calculate_total_tvl(&env), + active_users_24h: Self::count_active_users(&env, 86400), + transaction_volume_24h: Self::calculate_volume(&env, 86400), + health_factor_avg: Self::calculate_avg_health_factor(&env), + oracle_price_deviation: Self::calculate_price_deviation(&env), + gas_price_avg: Self::calculate_avg_gas_price(&env), + timestamp: env.ledger().timestamp(), + } + } + + + /// Alert conditions + pub fn check_alert_conditions( + env: Env, + metrics: &SystemMetrics + ) -> Vec { + let mut alerts = Vec::new(&env); + + // TVL drop alert + if metrics.total_value_locked < Self::get_tvl_threshold(&env) { + alerts.push_back(Alert { + level: AlertLevel::High, + message: "TVL dropped below threshold".into(), + timestamp: env.ledger().timestamp(), + }); + } + + // Price deviation alert + if metrics.oracle_price_deviation > 1000 { // 10% + alerts.push_back(Alert { + level: AlertLevel::Critical, + message: "High oracle price deviation detected".into(), + timestamp: env.ledger().timestamp(), + }); + } + + alerts + } + pub fn emergency_pause_all( env: Env, guardian: Address, @@ -160,4 +202,30 @@ pub struct TransactionRecord { pub timestamp: u64, pub gas_used: u32, pub block_number: u32, +} + +#[derive(Clone, Debug)] +pub struct SystemMetrics { + pub total_value_locked: i128, + pub active_users_24h: u32, + pub transaction_volume_24h: i128, + pub health_factor_avg: i128, + pub oracle_price_deviation: u32, + pub gas_price_avg: u32, + pub timestamp: u64, +} + +#[derive(Clone, Debug)] +pub struct Alert { + pub level: AlertLevel, + pub message: String, + pub timestamp: u64, +} + +#[derive(Clone, Debug)] +pub enum AlertLevel { + Low, + Medium, + High, + Critical, } \ No newline at end of file From b09b3c40f0f1fa7f36083b3083517fbd9846bb7f Mon Sep 17 00:00:00 2001 From: Josue Soto Date: Sun, 5 Oct 2025 18:36:03 -0600 Subject: [PATCH 3/4] Feat: Add cargo toml --- Cargo.lock | 7 ++++ Cargo.toml | 2 ++ contracts/access-control-manager/Cargo.toml | 30 ++++++++++++++++ .../src/{contract.rs => lib.rs} | 0 contracts/security-guardian/Cargo.toml | 30 ++++++++++++++++ .../src/{contract.rs => lib.rs} | 0 .../security-guardian/test/security_test.rs | 34 +++++++++++++++++++ 7 files changed, 103 insertions(+) create mode 100644 contracts/access-control-manager/Cargo.toml rename contracts/access-control-manager/src/{contract.rs => lib.rs} (100%) create mode 100644 contracts/security-guardian/Cargo.toml rename contracts/security-guardian/src/{contract.rs => lib.rs} (100%) create mode 100644 contracts/security-guardian/test/security_test.rs diff --git a/Cargo.lock b/Cargo.lock index 54efdf8..32dc05d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,13 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "access-control-manager" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "addr2line" version = "0.21.0" diff --git a/Cargo.toml b/Cargo.toml index 1a338a0..10818c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,8 @@ resolver = "2" members = [ "contracts/tbrg-token", "contracts/oracle", + "contracts/access-control-manager", + "contracts/security-guardian", "contracts/pool-factory" ] diff --git a/contracts/access-control-manager/Cargo.toml b/contracts/access-control-manager/Cargo.toml new file mode 100644 index 0000000..0a22d53 --- /dev/null +++ b/contracts/access-control-manager/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "access-control-manager" +version = "0.1.0" +authors = ["TrustBridge Team"] +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +soroban-sdk = "20.0.0" + +[dev-dependencies] +soroban-sdk = { version = "20.0.0", features = ["testutils"] } + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 +lto = true + +[profile.release-with-logs] +inherits = "release" +debug-assertions = true \ No newline at end of file diff --git a/contracts/access-control-manager/src/contract.rs b/contracts/access-control-manager/src/lib.rs similarity index 100% rename from contracts/access-control-manager/src/contract.rs rename to contracts/access-control-manager/src/lib.rs diff --git a/contracts/security-guardian/Cargo.toml b/contracts/security-guardian/Cargo.toml new file mode 100644 index 0000000..f5128e1 --- /dev/null +++ b/contracts/security-guardian/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "security-guardian" +version = "0.1.0" +authors = ["TrustBridge Team"] +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +soroban-sdk = "20.0.0" + +[dev-dependencies] +soroban-sdk = { version = "20.0.0", features = ["testutils"] } + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 +lto = true + +[profile.release-with-logs] +inherits = "release" +debug-assertions = true \ No newline at end of file diff --git a/contracts/security-guardian/src/contract.rs b/contracts/security-guardian/src/lib.rs similarity index 100% rename from contracts/security-guardian/src/contract.rs rename to contracts/security-guardian/src/lib.rs diff --git a/contracts/security-guardian/test/security_test.rs b/contracts/security-guardian/test/security_test.rs new file mode 100644 index 0000000..14d1f27 --- /dev/null +++ b/contracts/security-guardian/test/security_test.rs @@ -0,0 +1,34 @@ +#[cfg(test)] +mod security_tests { + use super::*; + + #[test] + fn test_flash_loan_attack_prevention() { + let env = TestEnvironment::new(); + let contracts = deploy_all_contracts(&env); + + // Simulate flash loan attack + let attack_result = simulate_flash_loan_attack(&env, &contracts); + assert!(attack_result.is_err()); + } + + #[test] + fn test_oracle_manipulation_protection() { + let env = TestEnvironment::new(); + let oracle = deploy_oracle_aggregator(&env); + + // Try to manipulate single oracle + let manipulation_result = attempt_oracle_manipulation(&env, &oracle); + assert!(manipulation_result.is_err()); + } + + #[test] + fn test_emergency_procedures() { + let env = TestEnvironment::new(); + let guardian = deploy_security_guardian(&env); + + // Test emergency pause + guardian.emergency_pause_all(&"test emergency".into()); + assert!(all_contracts_paused(&env)); + } +} \ No newline at end of file From 57ffbefa0c74d61292286fe9a6a022901b78fb9e Mon Sep 17 00:00:00 2001 From: Josue Soto Date: Sun, 5 Oct 2025 22:39:34 -0600 Subject: [PATCH 4/4] Feat: Add mev-protection contract --- contracts/mev-protection/Cargo.toml | 30 ++++ contracts/mev-protection/src/contract.rs | 187 +++++++++++++++++++++++ contracts/mev-protection/src/lib.rs | 0 3 files changed, 217 insertions(+) create mode 100644 contracts/mev-protection/Cargo.toml create mode 100644 contracts/mev-protection/src/contract.rs create mode 100644 contracts/mev-protection/src/lib.rs diff --git a/contracts/mev-protection/Cargo.toml b/contracts/mev-protection/Cargo.toml new file mode 100644 index 0000000..a6ce230 --- /dev/null +++ b/contracts/mev-protection/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "mev-protection" +version = "0.1.0" +authors = ["TrustBridge Team"] +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +soroban-sdk = "20.0.0" + +[dev-dependencies] +soroban-sdk = { version = "20.0.0", features = ["testutils"] } + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 +lto = true + +[profile.release-with-logs] +inherits = "release" +debug-assertions = true \ No newline at end of file diff --git a/contracts/mev-protection/src/contract.rs b/contracts/mev-protection/src/contract.rs new file mode 100644 index 0000000..d416f50 --- /dev/null +++ b/contracts/mev-protection/src/contract.rs @@ -0,0 +1,187 @@ +use soroban_sdk::{Address, Env, Symbol, Vec}; +use crate::{ProtectionConfig, TradeRecord}; + +// Storage keys +#[derive(Clone)] +pub enum DataKey { + Initialized, + Admin, + Config, + TradeHistory(Address, u32), // (asset, block) -> Vec + TradesCurrentBlock(Address), // trader -> count + CurrentBlock, // last processed block number + FlaggedAddress(Address), // address -> (bool, reason) + PoolLiquidity(Address), // asset -> liquidity amount + CurrentPrice(Address), // asset -> current price +} + +const DAY_IN_LEDGERS: u32 = 17280; +const INSTANCE_BUMP_AMOUNT: u32 = 7 * DAY_IN_LEDGERS; +const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; + +pub fn extend_instance(e: &Env) { + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); +} + +// Initialization +pub fn is_initialized(e: &Env) -> bool { + e.storage().instance().has(&DataKey::Initialized) +} + +pub fn set_initialized(e: &Env) { + e.storage().instance().set(&DataKey::Initialized, &true); +} + +// Admin +pub fn get_admin(e: &Env) -> Address { + e.storage().instance().get(&DataKey::Admin).unwrap() +} + +pub fn set_admin(e: &Env, admin: &Address) { + e.storage().instance().set(&DataKey::Admin, admin); +} + +// Configuration +pub fn get_config(e: &Env) -> ProtectionConfig { + e.storage().instance().get(&DataKey::Config).unwrap() +} + +pub fn set_config(e: &Env, config: &ProtectionConfig) { + e.storage().instance().set(&DataKey::Config, config); +} + +// Trade records +pub fn add_trade_record(e: &Env, trade: &TradeRecord) { + let block = trade.block; + let asset = trade.asset.clone(); + + let mut trades = get_trades_for_block(e, &asset, block); + trades.push_back(trade.clone()); + + e.storage() + .temporary() + .set(&DataKey::TradeHistory(asset, block), &trades); + + // Update current block tracker + update_current_block(e, block); + + // Cleanup old trades (keep last 100 blocks) + cleanup_old_trades(e, &asset, block); +} + +fn get_trades_for_block(e: &Env, asset: &Address, block: u32) -> Vec { + e.storage() + .temporary() + .get(&DataKey::TradeHistory(asset.clone(), block)) + .unwrap_or(Vec::new(e)) +} + +pub fn get_trades_in_window( + e: &Env, + asset: &Address, + start_block: u32, + end_block: u32, +) -> Vec { + let mut all_trades = Vec::new(e); + + for block in start_block..=end_block { + let trades = get_trades_for_block(e, asset, block); + for i in 0..trades.len() { + all_trades.push_back(trades.get(i).unwrap()); + } + } + + all_trades +} + +fn update_current_block(e: &Env, block: u32) { + let current = e.storage() + .instance() + .get(&DataKey::CurrentBlock) + .unwrap_or(0u32); + + if block > current { + e.storage().instance().set(&DataKey::CurrentBlock, &block); + // Reset trades count for new block + reset_all_trades_counts(e); + } +} + +fn cleanup_old_trades(e: &Env, asset: &Address, current_block: u32) { + if current_block > 100 { + let old_block = current_block - 100; + e.storage() + .temporary() + .remove(&DataKey::TradeHistory(asset.clone(), old_block)); + } +} + +// Trades per block tracking (for rate limiting) +pub fn get_trades_count_current_block(e: &Env, trader: &Address) -> u32 { + e.storage() + .temporary() + .get(&DataKey::TradesCurrentBlock(trader.clone())) + .unwrap_or(0u32) +} + +pub fn increment_trades_count_current_block(e: &Env, trader: &Address) { + let count = get_trades_count_current_block(e, trader); + e.storage() + .temporary() + .set(&DataKey::TradesCurrentBlock(trader.clone()), &(count + 1)); +} + +fn reset_all_trades_counts(e: &Env) { + // In production, you'd want to track all active traders + // For now, counts will naturally reset when block changes + // since we check current block in update_current_block +} + +// Flagged addresses +pub fn is_flagged(e: &Env, address: &Address) -> bool { + e.storage() + .persistent() + .has(&DataKey::FlaggedAddress(address.clone())) +} + +pub fn flag_address(e: &Env, address: &Address, reason: &Symbol) { + e.storage() + .persistent() + .set(&DataKey::FlaggedAddress(address.clone()), reason); +} + +pub fn unflag_address(e: &Env, address: &Address) { + e.storage() + .persistent() + .remove(&DataKey::FlaggedAddress(address.clone())); +} + +// Pool liquidity (updated by external oracle or pool contract) +pub fn get_pool_liquidity(e: &Env, asset: &Address) -> i128 { + e.storage() + .persistent() + .get(&DataKey::PoolLiquidity(asset.clone())) + .unwrap_or(1_000_000_000) // Default 1B for testing +} + +pub fn set_pool_liquidity(e: &Env, asset: &Address, liquidity: i128) { + e.storage() + .persistent() + .set(&DataKey::PoolLiquidity(asset.clone()), &liquidity); +} + +// Current price (updated by oracle) +pub fn get_current_price(e: &Env, asset: &Address) -> i128 { + e.storage() + .persistent() + .get(&DataKey::CurrentPrice(asset.clone())) + .unwrap_or(1_000_000) // Default price for testing +} + +pub fn set_current_price(e: &Env, asset: &Address, price: i128) { + e.storage() + .persistent() + .set(&DataKey::CurrentPrice(asset.clone()), &price); +} \ No newline at end of file diff --git a/contracts/mev-protection/src/lib.rs b/contracts/mev-protection/src/lib.rs new file mode 100644 index 0000000..e69de29