diff --git a/swaptrade-contracts/counter/Cargo.toml b/swaptrade-contracts/counter/Cargo.toml index 1f22b7b..66de450 100644 --- a/swaptrade-contracts/counter/Cargo.toml +++ b/swaptrade-contracts/counter/Cargo.toml @@ -13,6 +13,10 @@ doctest = false name = "formal_verification_tests" path = "tests/formal_verification_tests.rs" +[[test]] +name = "trading_comprehensive_test" +path = "tests/trading_comprehensive_test.rs" + [dependencies] soroban-sdk = { workspace = true } diff --git a/swaptrade-contracts/counter/src/lib.rs b/swaptrade-contracts/counter/src/lib.rs index 514e764..c63fa4f 100644 --- a/swaptrade-contracts/counter/src/lib.rs +++ b/swaptrade-contracts/counter/src/lib.rs @@ -363,9 +363,24 @@ impl CounterContract { } /// Swap tokens using simplified AMM (1:1 XLM <-> USDC-SIM) - pub fn swap(env: Env, from: Symbol, to: Symbol, amount: i128, user: Address) -> Result { + pub fn swap(env: Env, from: Symbol, to: Symbol, amount: i128, min_amount_out: i128, user: Address) -> Result { require_authenticated_verified_user(&env, &user)?; + // Oracle validation + use crate::oracle::{AggregatorV3Interface, OracleWrapper}; + let oracle = OracleWrapper; + let (price, timestamp) = oracle.latest_round_data(&env, (from.clone(), to.clone()))?; + + // Basic staleness check (e.g., 5 minutes = 300 seconds) + if env.ledger().timestamp().saturating_sub(timestamp) > 300 { + return Err(ContractError::StalePrice); + } + + // Minimal price check (price must be positive) + if price <= 0 { + return Err(ContractError::InvalidPrice); + } + let mut portfolio: Portfolio = env .storage() .instance() @@ -423,6 +438,13 @@ impl CounterContract { let fee_amount = (amount * fee_bps as i128) / 10000; let swap_amount = amount - fee_amount; + // Calculate oracle-based minimum amount + let expected_min_amount = (swap_amount as u128 * price) / crate::trading::PRECISION; + let slippage_tolerance_bps = 500; // 5% + let oracle_min_amount = expected_min_amount * (10000 - slippage_tolerance_bps) / 10000; + + let required_min = core::cmp::max(min_amount_out as u128, oracle_min_amount); + // Collect the fee if fee_amount > 0 { // Deduct from user @@ -448,6 +470,10 @@ impl CounterContract { swap_amount, user.clone(), ); + + if out_amount < required_min as i128 { + return Err(ContractError::SlippageExceeded); + } portfolio.record_trade(&env, user.clone()); diff --git a/swaptrade-contracts/counter/src/oracle.rs b/swaptrade-contracts/counter/src/oracle.rs index 7838f81..2ca4f29 100644 --- a/swaptrade-contracts/counter/src/oracle.rs +++ b/swaptrade-contracts/counter/src/oracle.rs @@ -18,10 +18,18 @@ pub struct PriceData { pub timestamp: u64, } -pub trait PriceFeed { - fn get_price(env: &Env, token_pair: (Symbol, Symbol)) -> Result; - fn last_update_time(env: &Env, token_pair: (Symbol, Symbol)) -> u64; - fn set_price(env: &Env, token_pair: (Symbol, Symbol), price: u128); +pub trait AggregatorV3Interface { + fn latest_round_data(&self, env: &Env, token_pair: (Symbol, Symbol)) -> Result<(i128, u64), ContractError>; +} + +pub struct OracleWrapper; + +impl AggregatorV3Interface for OracleWrapper { + fn latest_round_data(&self, env: &Env, token_pair: (Symbol, Symbol)) -> Result<(i128, u64), ContractError> { + let price = crate::oracle_adapter::OracleAdapter::get_price(env, token_pair)?; + let timestamp = env.ledger().timestamp(); + Ok((price as i128, timestamp)) + } } fn tolerance_key(pair: &(Symbol, Symbol)) -> (Symbol, Symbol, Symbol) { diff --git a/swaptrade-contracts/counter/trading.rs b/swaptrade-contracts/counter/trading.rs index 33974e2..59dee59 100644 --- a/swaptrade-contracts/counter/trading.rs +++ b/swaptrade-contracts/counter/trading.rs @@ -3,7 +3,7 @@ use soroban_sdk::{Env, Symbol, Address, symbol_short}; use crate::portfolio::{Portfolio, Asset}; use crate::oracle::{get_stored_price, ContractError}; -const PRECISION: u128 = 1_000_000_000_000_000_000; // 1e18 +pub const PRECISION: u128 = 1_000_000_000_000_000_000; // 1e18 const STALE_THRESHOLD_SECONDS: u64 = 600; // 10 minutes const LP_FEE_BPS: u128 = 30; // 0.3% = 30 basis points