diff --git a/swaptrade-contracts/counter/src/analytics_dashboard_tests.rs b/swaptrade-contracts/counter/src/analytics_dashboard_tests.rs index bfc3899..ddaa805 100644 --- a/swaptrade-contracts/counter/src/analytics_dashboard_tests.rs +++ b/swaptrade-contracts/counter/src/analytics_dashboard_tests.rs @@ -63,7 +63,7 @@ fn test_win_rate_calculation() { } let summary = portfolio.get_analytics_summary(&env, user); - + // Win rate should be 60% = 6_000_000 in fixed-point assert_eq!(summary.winning_trades, 3); assert_eq!(summary.losing_trades, 2); @@ -77,12 +77,36 @@ fn test_realized_pnl_tracking() { let user = Address::generate(&env); // Record multiple trades - portfolio.record_trade_with_pnl(&env, user.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 1000, 1200, 1000); - portfolio.record_trade_with_pnl(&env, user.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 1000, 800, 1000); - portfolio.record_trade_with_pnl(&env, user.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 1000, 1300, 1000); + portfolio.record_trade_with_pnl( + &env, + user.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 1000, + 1200, + 1000, + ); + portfolio.record_trade_with_pnl( + &env, + user.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 1000, + 800, + 1000, + ); + portfolio.record_trade_with_pnl( + &env, + user.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 1000, + 1300, + 1000, + ); let summary = portfolio.get_analytics_summary(&env, user); - + // Total PnL: 200 - 200 + 300 = 300 assert_eq!(summary.realized_pnl, 300); } @@ -93,12 +117,36 @@ fn test_best_and_worst_trade() { let mut portfolio = Portfolio::new(&env); let user = Address::generate(&env); - portfolio.record_trade_with_pnl(&env, user.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 1000, 1500, 1000); // +500 - portfolio.record_trade_with_pnl(&env, user.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 1000, 700, 1000); // -300 - portfolio.record_trade_with_pnl(&env, user.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 1000, 1200, 1000); // +200 + portfolio.record_trade_with_pnl( + &env, + user.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 1000, + 1500, + 1000, + ); // +500 + portfolio.record_trade_with_pnl( + &env, + user.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 1000, + 700, + 1000, + ); // -300 + portfolio.record_trade_with_pnl( + &env, + user.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 1000, + 1200, + 1000, + ); // +200 let summary = portfolio.get_analytics_summary(&env, user); - + assert_eq!(summary.best_trade, 500); assert_eq!(summary.worst_trade, -300); } @@ -110,15 +158,39 @@ fn test_avg_trade_metrics() { let user = Address::generate(&env); // Record trades with different sizes - portfolio.record_trade_with_pnl(&env, user.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 1000, 1100, 1000); - portfolio.record_trade_with_pnl(&env, user.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 2000, 2200, 1000); - portfolio.record_trade_with_pnl(&env, user.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 3000, 3300, 1000); + portfolio.record_trade_with_pnl( + &env, + user.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 1000, + 1100, + 1000, + ); + portfolio.record_trade_with_pnl( + &env, + user.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 2000, + 2200, + 1000, + ); + portfolio.record_trade_with_pnl( + &env, + user.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 3000, + 3300, + 1000, + ); let summary = portfolio.get_analytics_summary(&env, user); - + // Average trade size: (1000 + 2000 + 3000) / 3 = 2000 assert_eq!(summary.avg_trade_size, 2000); - + // Average winning trade: (100 + 200 + 300) / 3 = 200 assert_eq!(summary.avg_winning_trade, 200); } @@ -130,7 +202,7 @@ fn test_empty_analytics_summary() { let user = Address::generate(&env); let summary = portfolio.get_analytics_summary(&env, user); - + assert_eq!(summary.total_trades, 0); assert_eq!(summary.winning_trades, 0); assert_eq!(summary.losing_trades, 0); @@ -159,7 +231,7 @@ fn test_sharpe_ratio_calculation() { } let summary = portfolio.get_analytics_summary(&env, user); - + // Sharpe ratio should be positive assert!(summary.sharpe_ratio > 0); } @@ -171,12 +243,36 @@ fn test_max_drawdown_calculation() { let user = Address::generate(&env); // Record trades that create a drawdown scenario - portfolio.record_trade_with_pnl(&env, user.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 10000, 11000, 1000); // +1000 - portfolio.record_trade_with_pnl(&env, user.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 10000, 10500, 1000); // +500 - portfolio.record_trade_with_pnl(&env, user.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 10000, 9000, 1000); // -1000 + portfolio.record_trade_with_pnl( + &env, + user.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 10000, + 11000, + 1000, + ); // +1000 + portfolio.record_trade_with_pnl( + &env, + user.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 10000, + 10500, + 1000, + ); // +500 + portfolio.record_trade_with_pnl( + &env, + user.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 10000, + 9000, + 1000, + ); // -1000 let summary = portfolio.get_analytics_summary(&env, user); - + // Max drawdown should be calculated assert!(summary.max_drawdown >= 0); } @@ -208,19 +304,51 @@ fn test_multiple_users_analytics() { let user2 = Address::generate(&env); // User 1: profitable trader - portfolio.record_trade_with_pnl(&env, user1.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 1000, 1500, 1000); - portfolio.record_trade_with_pnl(&env, user1.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 1000, 1600, 1000); + portfolio.record_trade_with_pnl( + &env, + user1.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 1000, + 1500, + 1000, + ); + portfolio.record_trade_with_pnl( + &env, + user1.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 1000, + 1600, + 1000, + ); // User 2: losing trader - portfolio.record_trade_with_pnl(&env, user2.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 1000, 800, 1000); - portfolio.record_trade_with_pnl(&env, user2.clone(), symbol_short!("XLM"), symbol_short!("USDC"), 1000, 700, 1000); + portfolio.record_trade_with_pnl( + &env, + user2.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 1000, + 800, + 1000, + ); + portfolio.record_trade_with_pnl( + &env, + user2.clone(), + symbol_short!("XLM"), + symbol_short!("USDC"), + 1000, + 700, + 1000, + ); let summary1 = portfolio.get_analytics_summary(&env, user1); let summary2 = portfolio.get_analytics_summary(&env, user2); // User 1 should have positive PnL assert!(summary1.realized_pnl > 0); - + // User 2 should have negative PnL assert!(summary2.realized_pnl < 0); } @@ -249,9 +377,9 @@ fn test_trade_history_storage() { .trade_history .get(user.clone()) .unwrap_or_else(|| Vec::new(&env)); - + assert_eq!(history.len(), 5); - + // Check first trade let first_trade = history.get(0).unwrap(); assert_eq!(first_trade.amount_in, 1000); diff --git a/swaptrade-contracts/counter/src/bridge.rs b/swaptrade-contracts/counter/src/bridge.rs index 5acf858..baa9eb0 100644 --- a/swaptrade-contracts/counter/src/bridge.rs +++ b/swaptrade-contracts/counter/src/bridge.rs @@ -107,4 +107,4 @@ pub fn get_bridge_request(env: &Env, request_id: u64) -> BridgeRequest { .persistent() .get(&key) .expect("Bridge request not found") -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/errors.rs b/swaptrade-contracts/counter/src/errors.rs index 479456f..94b1be5 100644 --- a/swaptrade-contracts/counter/src/errors.rs +++ b/swaptrade-contracts/counter/src/errors.rs @@ -17,6 +17,8 @@ use soroban_sdk::contracterror; pub enum SwapTradeError { // ── Admin / access control ────────────────────────────────────────────── NotAdmin = 1, + NotAuthorized = 2, + InvalidAddress = 3, // ── Trading / contract state ──────────────────────────────────────────── TradingPaused = 10, @@ -93,4 +95,4 @@ pub enum SwapTradeError { } /// Alias kept for modules that still import `ContractError` by name. -pub type ContractError = SwapTradeError; \ No newline at end of file +pub type ContractError = SwapTradeError; diff --git a/swaptrade-contracts/counter/src/events.rs b/swaptrade-contracts/counter/src/events.rs index 6bcf0b9..435acd9 100644 --- a/swaptrade-contracts/counter/src/events.rs +++ b/swaptrade-contracts/counter/src/events.rs @@ -99,6 +99,43 @@ impl Events { .publish((Symbol::new(env, "AdminPaused"), admin), (timestamp,)); } + pub fn fees_collected(env: &Env, token: Symbol, amount: i128, pool_id: u64) { + env.events().publish( + (Symbol::new(env, "FeesCollected"), token, pool_id), + (amount, env.ledger().timestamp()), + ); + } + + pub fn fee_parameters_updated( + env: &Env, + pool_id: u64, + new_fee_rate: u32, + new_treasury: Option
, + ) { + env.events().publish( + (Symbol::new(env, "FeeParametersUpdated"), pool_id), + (new_fee_rate, new_treasury, env.ledger().timestamp()), + ); + } + + pub fn fees_distributed( + env: &Env, + pool_id: u64, + token: Symbol, + amount: i128, + recipient: Address, + ) { + env.events().publish( + ( + Symbol::new(env, "FeesDistributed"), + token, + pool_id, + recipient, + ), + (amount, env.ledger().timestamp()), + ); + } + pub fn admin_resumed(env: &Env, admin: Address, timestamp: i64) { env.events() .publish((Symbol::new(env, "AdminResumed"), admin), (timestamp,)); diff --git a/swaptrade-contracts/counter/src/farming.rs b/swaptrade-contracts/counter/src/farming.rs index 11c7f03..e7135fa 100644 --- a/swaptrade-contracts/counter/src/farming.rs +++ b/swaptrade-contracts/counter/src/farming.rs @@ -1,9 +1,9 @@ +use crate::errors::SwapTradeError; /// Yield Farming / Liquidity Mining Module /// /// Rewards users for staking LP tokens over time using an accumulator-per-share /// pattern that ensures proportional reward distribution. use soroban_sdk::{contracttype, symbol_short, Address, Env, Map}; -use crate::errors::SwapTradeError; // ──────────────────────────────────────────────────────────────────────────── // Constants @@ -78,7 +78,8 @@ impl FarmingManager { /// Get the current admin address fn get_admin(env: &Env) -> Address { - env.storage().persistent() + env.storage() + .persistent() .get(&FarmingKey::Admin) .expect("Farming module not initialized") } @@ -89,7 +90,9 @@ impl FarmingManager { /// Update the pool's reward accumulator - must be called before any state changes fn update_pool_accumulator(env: &Env, pool_id: u64) -> Result<(), SwapTradeError> { - let mut pool_state = env.storage().persistent() + let mut pool_state = env + .storage() + .persistent() .get(&FarmingKey::PoolState(pool_id)) .unwrap_or_else(|| PoolFarmState { total_staked_lp: 0, @@ -102,48 +105,61 @@ impl FarmingManager { if pool_state.total_staked_lp == 0 || pool_state.emission_rate == 0 { // No stakers or no emissions, just update the timestamp pool_state.last_update_timestamp = env.ledger().timestamp(); - env.storage().persistent().set(&FarmingKey::PoolState(pool_id), &pool_state); + env.storage() + .persistent() + .set(&FarmingKey::PoolState(pool_id), &pool_state); return Ok(()); } let current_timestamp = env.ledger().timestamp(); let time_elapsed = current_timestamp - pool_state.last_update_timestamp; - + if time_elapsed == 0 { return Ok(()); } // Calculate rewards generated during this period let new_rewards = (time_elapsed as i128) * pool_state.emission_rate; - + // Calculate the additional reward per share (scaled to maintain precision) - let reward_per_share_increase = (new_rewards * Self::SCALE_FACTOR) / pool_state.total_staked_lp; - + let reward_per_share_increase = + (new_rewards * Self::SCALE_FACTOR) / pool_state.total_staked_lp; + // Update the accumulator pool_state.reward_per_share_accumulator += reward_per_share_increase; pool_state.last_update_timestamp = current_timestamp; pool_state.total_rewards_distributed += new_rewards; // Save the updated pool state - env.storage().persistent().set(&FarmingKey::PoolState(pool_id), &pool_state); + env.storage() + .persistent() + .set(&FarmingKey::PoolState(pool_id), &pool_state); // Update global total - let mut global_total: i128 = env.storage().persistent() + let mut global_total: i128 = env + .storage() + .persistent() .get(&FarmingKey::TotalRewardsDistributed) .unwrap_or(0); global_total += new_rewards; - env.storage().persistent().set(&FarmingKey::TotalRewardsDistributed, &global_total); + env.storage() + .persistent() + .set(&FarmingKey::TotalRewardsDistributed, &global_total); Ok(()) } /// Update a user's pending rewards based on the current pool accumulator fn update_user_position(env: &Env, pool_id: u64, user: Address) -> Result<(), SwapTradeError> { - let pool_state = env.storage().persistent() + let pool_state = env + .storage() + .persistent() .get(&FarmingKey::PoolState(pool_id)) .ok_or(SwapTradeError::LPPositionNotFound)?; - let mut user_position = env.storage().persistent() + let mut user_position = env + .storage() + .persistent() .get(&FarmingKey::UserPosition(pool_id, user.clone())) .unwrap_or_else(|| UserFarmPosition { staked_lp_amount: 0, @@ -155,15 +171,19 @@ impl FarmingManager { if user_position.staked_lp_amount > 0 { // Calculate the accumulated rewards since last update - let accumulated_rewards = ((pool_state.reward_per_share_accumulator - user_position.reward_per_share_debt) - * user_position.staked_lp_amount) / Self::SCALE_FACTOR; + let accumulated_rewards = ((pool_state.reward_per_share_accumulator + - user_position.reward_per_share_debt) + * user_position.staked_lp_amount) + / Self::SCALE_FACTOR; user_position.pending_rewards += accumulated_rewards; } // Update the user's debt to the current pool accumulator user_position.reward_per_share_debt = pool_state.reward_per_share_accumulator; - - env.storage().persistent().set(&FarmingKey::UserPosition(pool_id, user), &user_position); + + env.storage() + .persistent() + .set(&FarmingKey::UserPosition(pool_id, user), &user_position); Ok(()) } @@ -186,7 +206,9 @@ impl FarmingManager { Self::update_user_position(env, pool_id, user.clone())?; // Get and update pool state - let mut pool_state = env.storage().persistent() + let mut pool_state = env + .storage() + .persistent() .get(&FarmingKey::PoolState(pool_id)) .unwrap_or_else(|| PoolFarmState { total_staked_lp: 0, @@ -197,7 +219,9 @@ impl FarmingManager { }); // Get and update user position - let mut user_position = env.storage().persistent() + let mut user_position = env + .storage() + .persistent() .get(&FarmingKey::UserPosition(pool_id, user.clone())) .unwrap_or_else(|| UserFarmPosition { staked_lp_amount: 0, @@ -213,8 +237,13 @@ impl FarmingManager { user_position.is_active = true; // Save updated states - env.storage().persistent().set(&FarmingKey::PoolState(pool_id), &pool_state); - env.storage().persistent().set(&FarmingKey::UserPosition(pool_id, user.clone()), &user_position); + env.storage() + .persistent() + .set(&FarmingKey::PoolState(pool_id), &pool_state); + env.storage().persistent().set( + &FarmingKey::UserPosition(pool_id, user.clone()), + &user_position, + ); // Emit event env.events().publish( @@ -243,7 +272,9 @@ impl FarmingManager { Self::update_user_position(env, pool_id, user.clone())?; // Get user position - let mut user_position = env.storage().persistent() + let mut user_position = env + .storage() + .persistent() .get(&FarmingKey::UserPosition(pool_id, user.clone())) .ok_or(SwapTradeError::LPPositionNotFound)?; @@ -252,21 +283,28 @@ impl FarmingManager { } // Get and update pool state - let mut pool_state = env.storage().persistent() + let mut pool_state = env + .storage() + .persistent() .get(&FarmingKey::PoolState(pool_id)) .ok_or(SwapTradeError::LPPositionNotFound)?; // Update totals pool_state.total_staked_lp -= amount; user_position.staked_lp_amount -= amount; - + if user_position.staked_lp_amount == 0 { user_position.is_active = false; } // Save updated states - env.storage().persistent().set(&FarmingKey::PoolState(pool_id), &pool_state); - env.storage().persistent().set(&FarmingKey::UserPosition(pool_id, user.clone()), &user_position); + env.storage() + .persistent() + .set(&FarmingKey::PoolState(pool_id), &pool_state); + env.storage().persistent().set( + &FarmingKey::UserPosition(pool_id, user.clone()), + &user_position, + ); // Emit event env.events().publish( @@ -290,7 +328,9 @@ impl FarmingManager { Self::update_user_position(env, pool_id, user.clone())?; // Get user position - let mut user_position = env.storage().persistent() + let mut user_position = env + .storage() + .persistent() .get(&FarmingKey::UserPosition(pool_id, user.clone())) .ok_or(SwapTradeError::LPPositionNotFound)?; @@ -303,7 +343,10 @@ impl FarmingManager { user_position.pending_rewards = 0; // Zero out pending rewards after claim // Save the updated position - env.storage().persistent().set(&FarmingKey::UserPosition(pool_id, user.clone()), &user_position); + env.storage().persistent().set( + &FarmingKey::UserPosition(pool_id, user.clone()), + &user_position, + ); // Emit event env.events().publish( @@ -325,7 +368,9 @@ impl FarmingManager { Self::update_pool_accumulator(env, pool_id)?; Self::update_user_position(env, pool_id, user.clone())?; - let user_position = env.storage().persistent() + let user_position = env + .storage() + .persistent() .get(&FarmingKey::UserPosition(pool_id, user)) .ok_or(SwapTradeError::LPPositionNotFound)?; @@ -340,7 +385,7 @@ impl FarmingManager { admin: Address, ) -> Result<(), SwapTradeError> { admin.require_auth(); - + let current_admin = Self::get_admin(env); if admin != current_admin { return Err(SwapTradeError::NotAdmin); @@ -355,7 +400,9 @@ impl FarmingManager { Self::update_pool_accumulator(env, pool_id)?; // Get and update pool state - let mut pool_state = env.storage().persistent() + let mut pool_state = env + .storage() + .persistent() .get(&FarmingKey::PoolState(pool_id)) .unwrap_or_else(|| PoolFarmState { total_staked_lp: 0, @@ -367,7 +414,9 @@ impl FarmingManager { let old_rate = pool_state.emission_rate; pool_state.emission_rate = new_emission_rate; - env.storage().persistent().set(&FarmingKey::PoolState(pool_id), &pool_state); + env.storage() + .persistent() + .set(&FarmingKey::PoolState(pool_id), &pool_state); // Emit event env.events().publish( @@ -382,9 +431,10 @@ impl FarmingManager { pub fn get_pool_state(env: &Env, pool_id: u64) -> Result { // Update before returning to ensure latest state Self::update_pool_accumulator(env, pool_id)?; - - env.storage().persistent() + + env.storage() + .persistent() .get(&FarmingKey::PoolState(pool_id)) .ok_or(SwapTradeError::LPPositionNotFound) } -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/farming_tests.rs b/swaptrade-contracts/counter/src/farming_tests.rs index 19417ad..f1b1668 100644 --- a/swaptrade-contracts/counter/src/farming_tests.rs +++ b/swaptrade-contracts/counter/src/farming_tests.rs @@ -1,8 +1,8 @@ #[cfg(test)] use super::*; -use soroban_sdk::{Env, Address, testutils::Address as _}; -use crate::farming::FarmingManager; use crate::errors::SwapTradeError; +use crate::farming::FarmingManager; +use soroban_sdk::{testutils::Address as _, Address, Env}; #[test] fn test_farming_proportional_rewards() { @@ -10,40 +10,40 @@ fn test_farming_proportional_rewards() { let admin = Address::generate(&env); let user1 = Address::generate(&env); let user2 = Address::generate(&env); - + // Initialize farming module FarmingManager::initialize(&env, admin.clone()); - + let pool_id: u64 = 1; let emission_rate: i128 = 10; // 10 reward tokens per second - + // Admin sets emission rate FarmingManager::set_farm_emission_rate(&env, pool_id, emission_rate, admin.clone()).unwrap(); - + // User1 stakes 100 LP tokens user1.require_auth(); FarmingManager::stake_lp(&env, pool_id, 100, user1.clone()).unwrap(); - + // Advance time by 100 seconds env.ledger().set_timestamp(env.ledger().timestamp() + 100); - + // User2 stakes 200 LP tokens (total staked now 300) user2.require_auth(); FarmingManager::stake_lp(&env, pool_id, 200, user2.clone()).unwrap(); - + // Advance time by another 100 seconds (total 200 seconds) env.ledger().set_timestamp(env.ledger().timestamp() + 100); - + // Calculate expected rewards: // User1: first 100s: 100% of 10/s * 100s = 1000 // next 100s: 1/3 of 10/s * 100s = ~333.333 // Total user1: 1333 // User2: only second 100s: 2/3 of 10/s *100s = ~666.666 // Total user2: 666 - + let pending1 = FarmingManager::get_pending_farm_rewards(&env, pool_id, user1.clone()).unwrap(); let pending2 = FarmingManager::get_pending_farm_rewards(&env, pool_id, user2.clone()).unwrap(); - + assert!(pending1 > pending2); assert_eq!(pending1, 1333); assert_eq!(pending2, 666); @@ -55,27 +55,27 @@ fn test_claim_and_double_claim() { let env = Env::default(); let admin = Address::generate(&env); let user = Address::generate(&env); - + FarmingManager::initialize(&env, admin.clone()); - + let pool_id: u64 = 1; FarmingManager::set_farm_emission_rate(&env, pool_id, 10, admin.clone()).unwrap(); - + // User stakes 100 LP user.require_auth(); FarmingManager::stake_lp(&env, pool_id, 100, user.clone()).unwrap(); - + // Advance time env.ledger().set_timestamp(env.ledger().timestamp() + 100); - + // First claim should work let claimed = FarmingManager::claim_farm_rewards(&env, pool_id, user.clone()).unwrap(); assert_eq!(claimed, 1000); - + // Second claim should return error (no rewards left) let result = FarmingManager::claim_farm_rewards(&env, pool_id, user.clone()); assert!(matches!(result, Err(SwapTradeError::NoClaimableBonuses))); - + // Pending rewards should be 0 let pending = FarmingManager::get_pending_farm_rewards(&env, pool_id, user.clone()).unwrap(); assert_eq!(pending, 0); @@ -86,26 +86,26 @@ fn test_unstake_pays_accrued_rewards() { let env = Env::default(); let admin = Address::generate(&env); let user = Address::generate(&env); - + FarmingManager::initialize(&env, admin.clone()); - + let pool_id: u64 = 1; FarmingManager::set_farm_emission_rate(&env, pool_id, 10, admin.clone()).unwrap(); - + // Stake user.require_auth(); FarmingManager::stake_lp(&env, pool_id, 100, user.clone()).unwrap(); - + // Wait 50 seconds env.ledger().set_timestamp(env.ledger().timestamp() + 50); - + // Unstake half FarmingManager::unstake_lp(&env, pool_id, 50, user.clone()).unwrap(); - + // Check rewards are accrued let pending = FarmingManager::get_pending_farm_rewards(&env, pool_id, user.clone()).unwrap(); assert_eq!(pending, 500); - + // Wait another 50 seconds - only 50 LP still staked, so should accumulate another 500 env.ledger().set_timestamp(env.ledger().timestamp() + 50); let pending = FarmingManager::get_pending_farm_rewards(&env, pool_id, user.clone()).unwrap(); @@ -117,25 +117,25 @@ fn test_emission_rate_change_only_affects_future() { let env = Env::default(); let admin = Address::generate(&env); let user = Address::generate(&env); - + FarmingManager::initialize(&env, admin.clone()); - + let pool_id: u64 = 1; // Initial rate: 10 per second FarmingManager::set_farm_emission_rate(&env, pool_id, 10, admin.clone()).unwrap(); - + user.require_auth(); FarmingManager::stake_lp(&env, pool_id, 100, user.clone()).unwrap(); - + // First 100 seconds with rate 10/s: should get 1000 rewards env.ledger().set_timestamp(env.ledger().timestamp() + 100); - + // Admin updates rate to 20 per second FarmingManager::set_farm_emission_rate(&env, pool_id, 20, admin.clone()).unwrap(); - + // Another 100 seconds with rate 20/s: should get another 2000 rewards env.ledger().set_timestamp(env.ledger().timestamp() + 100); - + let pending = FarmingManager::get_pending_farm_rewards(&env, pool_id, user.clone()).unwrap(); assert_eq!(pending, 3000); // Total 3000 = 1000 + 2000 } @@ -145,12 +145,12 @@ fn test_invalid_stake_amount() { let env = Env::default(); let admin = Address::generate(&env); let user = Address::generate(&env); - + FarmingManager::initialize(&env, admin.clone()); - + let pool_id: u64 = 1; FarmingManager::set_farm_emission_rate(&env, pool_id, 10, admin.clone()).unwrap(); - + // Try to stake less than minimum user.require_auth(); let result = FarmingManager::stake_lp(&env, pool_id, 50, user.clone()); @@ -162,11 +162,11 @@ fn test_non_admin_cannot_set_emission_rate() { let env = Env::default(); let admin = Address::generate(&env); let non_admin = Address::generate(&env); - + FarmingManager::initialize(&env, admin.clone()); - + let pool_id: u64 = 1; non_admin.require_auth(); let result = FarmingManager::set_farm_emission_rate(&env, pool_id, 20, non_admin.clone()); assert!(matches!(result, Err(SwapTradeError::NotAdmin))); -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/governance_params.rs b/swaptrade-contracts/counter/src/governance_params.rs index 4a5764a..64623c7 100644 --- a/swaptrade-contracts/counter/src/governance_params.rs +++ b/swaptrade-contracts/counter/src/governance_params.rs @@ -163,9 +163,10 @@ impl GovernanceParams { } // Commit only the targeted parameter — isolated write (#156). - env.storage() - .persistent() - .set(&GovParamStorageKey::ParamValue(update.param.clone()), &update.new_value); + env.storage().persistent().set( + &GovParamStorageKey::ParamValue(update.param.clone()), + &update.new_value, + ); update.executed = true; env.storage() @@ -247,13 +248,7 @@ mod tests { fn test_propose_and_execute_after_timelock() { let (env, contract_id, admin) = setup(); env.as_contract(&contract_id, || { - let id = GovernanceParams::propose_update( - &env, - &admin, - ParamKey::FeeBps, - 300, - ) - .unwrap(); + let id = GovernanceParams::propose_update(&env, &admin, ParamKey::FeeBps, 300).unwrap(); // Execution before delay must fail. assert_eq!( @@ -319,8 +314,14 @@ mod tests { GovernanceParams::execute_update(&env, &admin, id).unwrap(); // MaxSwapAmount must remain unset — no side effects. - assert_eq!(GovernanceParams::get_param(&env, ParamKey::MaxSwapAmount), None); - assert_eq!(GovernanceParams::get_param(&env, ParamKey::FeeBps), Some(200)); + assert_eq!( + GovernanceParams::get_param(&env, ParamKey::MaxSwapAmount), + None + ); + assert_eq!( + GovernanceParams::get_param(&env, ParamKey::FeeBps), + Some(200) + ); }); } } diff --git a/swaptrade-contracts/counter/src/governance_system.rs b/swaptrade-contracts/counter/src/governance_system.rs index 2c78ee0..14d5891 100644 --- a/swaptrade-contracts/counter/src/governance_system.rs +++ b/swaptrade-contracts/counter/src/governance_system.rs @@ -1,7 +1,7 @@ -use soroban_sdk::{contracttype, Address, Env, Symbol, Vec, Map}; use crate::errors::SwapTradeError; -use crate::governance_types::*; use crate::events::Events; +use crate::governance_types::*; +use soroban_sdk::{contracttype, Address, Env, Map, Symbol, Vec}; /// On-chain governance system for SwapTrade pub struct GovernanceSystem; @@ -26,7 +26,8 @@ impl GovernanceSystem { } // Check proposal cooldown - let last_proposal_time = env.storage() + let last_proposal_time = env + .storage() .persistent() .get(&GovernanceKey::VoterLastProposal(proposer.clone())) .unwrap_or(0u64); @@ -58,7 +59,8 @@ impl GovernanceSystem { votes_against: 0, votes_abstain: 0, total_voting_power: Self::get_total_voting_power(env), - quorum_required: (config.quorum_threshold as u128 * Self::get_total_voting_power(env)) / 10000, + quorum_required: (config.quorum_threshold as u128 * Self::get_total_voting_power(env)) + / 10000, approval_threshold: config.approval_threshold, executed: false, }; @@ -69,9 +71,10 @@ impl GovernanceSystem { .set(&GovernanceKey::Proposal(proposal_id), &proposal); // Update last proposal time - env.storage() - .persistent() - .set(&GovernanceKey::VoterLastProposal(proposer.clone()), ¤t_time); + env.storage().persistent().set( + &GovernanceKey::VoterLastProposal(proposer.clone()), + ¤t_time, + ); // Emit event env.events().publish( @@ -105,7 +108,8 @@ impl GovernanceSystem { // Check if voter already voted let votes_key = GovernanceKey::ProposalVotes(proposal_id); - let mut votes: Map = env.storage() + let mut votes: Map = env + .storage() .persistent() .get(&votes_key) .unwrap_or_else(|| Map::new(env)); @@ -139,7 +143,9 @@ impl GovernanceSystem { // Store updated data env.storage().persistent().set(&votes_key, &votes); - env.storage().persistent().set(&GovernanceKey::Proposal(proposal_id), &proposal); + env.storage() + .persistent() + .set(&GovernanceKey::Proposal(proposal_id), &proposal); // Emit event env.events().publish( @@ -192,7 +198,9 @@ impl GovernanceSystem { } else { // Set execution time if not set proposal.execution_time = Some(current_time + config.execution_delay); - env.storage().persistent().set(&GovernanceKey::Proposal(proposal_id), &proposal); + env.storage() + .persistent() + .set(&GovernanceKey::Proposal(proposal_id), &proposal); return Err(SwapTradeError::InvalidAmount); // Not ready for execution yet } @@ -202,7 +210,9 @@ impl GovernanceSystem { // Mark as executed proposal.executed = true; proposal.status = ProposalStatus::Executed; - env.storage().persistent().set(&GovernanceKey::Proposal(proposal_id), &proposal); + env.storage() + .persistent() + .set(&GovernanceKey::Proposal(proposal_id), &proposal); // Emit event env.events().publish( @@ -235,7 +245,9 @@ impl GovernanceSystem { } proposal.status = ProposalStatus::Cancelled; - env.storage().persistent().set(&GovernanceKey::Proposal(proposal_id), &proposal); + env.storage() + .persistent() + .set(&GovernanceKey::Proposal(proposal_id), &proposal); // Emit event env.events().publish( @@ -271,7 +283,11 @@ impl GovernanceSystem { } /// Set governance configuration (admin only) - pub fn set_config(env: &Env, admin: &Address, config: &GovernanceConfig) -> Result<(), SwapTradeError> { + pub fn set_config( + env: &Env, + admin: &Address, + config: &GovernanceConfig, + ) -> Result<(), SwapTradeError> { admin.require_auth(); crate::admin::require_admin(env, admin)?; @@ -283,7 +299,9 @@ impl GovernanceSystem { return Err(SwapTradeError::InvalidAmount); } - env.storage().persistent().set(&GovernanceKey::Config, config); + env.storage() + .persistent() + .set(&GovernanceKey::Config, config); Ok(()) } @@ -333,27 +351,41 @@ impl GovernanceSystem { } } - fn execute_parameter_change(env: &Env, param_key: &ParamKey, new_value: i128) -> Result<(), SwapTradeError> { + fn execute_parameter_change( + env: &Env, + param_key: &ParamKey, + new_value: i128, + ) -> Result<(), SwapTradeError> { match param_key { ParamKey::MaxSwapAmount => { // Update max swap amount - env.storage().instance().set(&Symbol::short("max_swap"), &new_value); + env.storage() + .instance() + .set(&Symbol::short("max_swap"), &new_value); } ParamKey::FeeBps => { // Update fee - env.storage().instance().set(&Symbol::short("fee_bps"), &(new_value as u32)); + env.storage() + .instance() + .set(&Symbol::short("fee_bps"), &(new_value as u32)); } ParamKey::RateLimitWindow => { // Update rate limit window - env.storage().instance().set(&Symbol::short("rate_win"), &new_value); + env.storage() + .instance() + .set(&Symbol::short("rate_win"), &new_value); } ParamKey::MaxSlippageBps => { // Update max slippage - env.storage().instance().set(&Symbol::short("max_slip"), &(new_value as u32)); + env.storage() + .instance() + .set(&Symbol::short("max_slip"), &(new_value as u32)); } ParamKey::EmergencyPause => { // Emergency pause/unpause - env.storage().instance().set(&Symbol::short("paused"), &(new_value != 0)); + env.storage() + .instance() + .set(&Symbol::short("paused"), &(new_value != 0)); } ParamKey::RiskConfigMaxPosition => { // Update risk config @@ -389,7 +421,9 @@ impl GovernanceSystem { } fn execute_emergency_action(env: &Env, pause: bool) -> Result<(), SwapTradeError> { - env.storage().instance().set(&Symbol::short("paused"), &pause); + env.storage() + .instance() + .set(&Symbol::short("paused"), &pause); Ok(()) } @@ -405,7 +439,8 @@ impl GovernanceSystem { } fn get_next_proposal_id(env: &Env) -> u64 { - let current_id: u64 = env.storage() + let current_id: u64 = env + .storage() .persistent() .get(&GovernanceKey::NextProposalId) .unwrap_or(1); @@ -416,4 +451,4 @@ impl GovernanceSystem { current_id } -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/governance_tests.rs b/swaptrade-contracts/counter/src/governance_tests.rs index ef7d7ab..a562b05 100644 --- a/swaptrade-contracts/counter/src/governance_tests.rs +++ b/swaptrade-contracts/counter/src/governance_tests.rs @@ -1,11 +1,10 @@ #[cfg(test)] mod governance_tests { use super::*; - use crate::CounterContract; - use soroban_sdk::{testutils::Address as _, testutils::Ledger as _, Address, Env, Symbol, symbol_short}; - use crate::governance_types::*; use crate::governance_system::GovernanceSystem; + use crate::governance_types::*; use crate::staking_bonus::StakingBonusManager; + use soroban_sdk::{symbol_short, testutils::Address as _, Address, Env, Symbol}; // ===== GOVERNANCE PROPOSAL TESTS ===== @@ -47,13 +46,8 @@ mod governance_tests { let proposal_type = ProposalType::ParameterChange(ParamKey::FeeBps, 25); let description = Symbol::new(&env, "test_proposal"); - let result = GovernanceSystem::create_proposal( - &env, - &proposer, - proposal_type, - description, - 86400, - ); + let result = + GovernanceSystem::create_proposal(&env, &proposer, proposal_type, description, 86400); assert!(result.is_err()); } @@ -99,15 +93,11 @@ mod governance_tests { proposal_type, symbol_short!("test"), 86400, - ).unwrap(); + ) + .unwrap(); // Cast vote - let result = GovernanceSystem::cast_vote( - &env, - &voter, - proposal_id, - VoteOption::For, - ); + let result = GovernanceSystem::cast_vote(&env, &voter, proposal_id, VoteOption::For); assert!(result.is_ok()); @@ -137,7 +127,8 @@ mod governance_tests { ProposalType::ParameterChange(ParamKey::FeeBps, 25), symbol_short!("test"), 86400, - ).unwrap(); + ) + .unwrap(); // Cast first vote GovernanceSystem::cast_vote(&env, &voter, proposal_id, VoteOption::For).unwrap(); @@ -167,7 +158,8 @@ mod governance_tests { ProposalType::ParameterChange(ParamKey::FeeBps, 25), Symbol::new(&env, "fee_change"), 86400, - ).unwrap(); + ) + .unwrap(); // Cast votes - 800 + 700 = 1500 votes for (>50% of 2000 total) GovernanceSystem::cast_vote(&env, &voter1, proposal_id, VoteOption::For).unwrap(); @@ -206,7 +198,8 @@ mod governance_tests { ProposalType::ParameterChange(ParamKey::FeeBps, 25), Symbol::new(&env, "fee_change"), 86400, - ).unwrap(); + ) + .unwrap(); // Cast votes - not enough for quorum (20% required) GovernanceSystem::cast_vote(&env, &voter1, proposal_id, VoteOption::For).unwrap(); @@ -235,7 +228,8 @@ mod governance_tests { ProposalType::ParameterChange(ParamKey::FeeBps, 25), symbol_short!("test"), 86400, - ).unwrap(); + ) + .unwrap(); // Cancel by proposer let result = GovernanceSystem::cancel_proposal(&env, &proposer, proposal_id); @@ -270,7 +264,8 @@ mod governance_tests { ProposalType::ParameterChange(ParamKey::FeeBps, 50), Symbol::new(&env, "fee_change"), 86400, - ).unwrap(); + ) + .unwrap(); // Vote for GovernanceSystem::cast_vote(&env, &voter, proposal_id, VoteOption::For).unwrap(); @@ -280,7 +275,11 @@ mod governance_tests { GovernanceSystem::execute_proposal(&env, &executor, proposal_id).unwrap(); // Verify fee was changed - let new_fee: u32 = env.storage().instance().get(&symbol_short!("fee_bps")).unwrap_or(30); + let new_fee: u32 = env + .storage() + .instance() + .get(&symbol_short!("fee_bps")) + .unwrap_or(30); assert_eq!(new_fee, 50); } @@ -300,10 +299,13 @@ mod governance_tests { let proposal_id = GovernanceSystem::create_proposal( &env, &proposer, - ProposalType::AdminUpgrade(new_admin.clone()), - Symbol::new(&env, "admin_upgrade"), + ProposalType::AdminUpgrade { + new_admin: new_admin.clone(), + }, + symbol_short!("admin_upgrade"), 86400, - ).unwrap(); + ) + .unwrap(); // Vote for GovernanceSystem::cast_vote(&env, &voter, proposal_id, VoteOption::For).unwrap(); @@ -379,7 +381,8 @@ mod governance_tests { ProposalType::ParameterChange(ParamKey::MaxSwapAmount, 1000000), Symbol::new(&env, "increase_max_swap"), 86400, - ).unwrap(); + ) + .unwrap(); // 2. Cast votes (3500 for, 800 abstain = 72% participation, 87% approval) CounterContract::cast_governance_vote( @@ -387,38 +390,42 @@ mod governance_tests { voter1.clone(), proposal_id, VoteOption::For, - ).unwrap(); + ) + .unwrap(); CounterContract::cast_governance_vote( env.clone(), voter2.clone(), proposal_id, VoteOption::For, - ).unwrap(); + ) + .unwrap(); CounterContract::cast_governance_vote( env.clone(), voter3.clone(), proposal_id, VoteOption::Abstain, - ).unwrap(); + ) + .unwrap(); // 3. Fast forward past voting period env.ledger().set_timestamp(env.ledger().timestamp() + 86500); // 4. Execute proposal - CounterContract::execute_governance_proposal( - env.clone(), - executor.clone(), - proposal_id, - ).unwrap(); + CounterContract::execute_governance_proposal(env.clone(), executor.clone(), proposal_id) + .unwrap(); // 5. Verify execution let proposal = CounterContract::get_governance_proposal(env.clone(), proposal_id).unwrap(); assert_eq!(proposal.status, ProposalStatus::Executed); // 6. Verify parameter change - let max_swap: i128 = env.storage().instance().get(&symbol_short!("max_swap")).unwrap_or(0); + let max_swap: i128 = env + .storage() + .instance() + .get(&symbol_short!("max_swap")) + .unwrap_or(0); assert_eq!(max_swap, 1000000); } @@ -440,7 +447,8 @@ mod governance_tests { ProposalType::EmergencyAction(true), Symbol::new(&env, "emergency_pause"), 86400, - ).unwrap(); + ) + .unwrap(); // Cast vote (only 9% participation) CounterContract::cast_governance_vote( @@ -448,7 +456,8 @@ mod governance_tests { voter.clone(), proposal_id, VoteOption::For, - ).unwrap(); + ) + .unwrap(); // Fast forward and try to execute env.ledger().set_timestamp(env.ledger().timestamp() + 86500); @@ -476,7 +485,8 @@ mod governance_tests { ProposalType::ParameterChange(ParamKey::FeeBps, 20), symbol_short!("proposal1"), 86400, - ).unwrap(); + ) + .unwrap(); // Try to create another proposal immediately - should fail let result = CounterContract::create_governance_proposal( @@ -489,4 +499,4 @@ mod governance_tests { assert!(result.is_err()); } -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/governance_types.rs b/swaptrade-contracts/counter/src/governance_types.rs index 5232c10..7b9f1d7 100644 --- a/swaptrade-contracts/counter/src/governance_types.rs +++ b/swaptrade-contracts/counter/src/governance_types.rs @@ -1,5 +1,5 @@ -use soroban_sdk::{contracttype, Address, Env, Symbol, Vec, Map}; use crate::errors::SwapTradeError; +use soroban_sdk::{contracttype, Address, Env, Map, Symbol, Vec}; /// Governance proposal types #[contracttype] @@ -8,11 +8,11 @@ pub enum ProposalType { /// Change protocol parameters (fees, limits, etc.) ParameterChange(ParamKey, i128), /// Upgrade admin address - AdminUpgrade(Address), + AdminUpgrade { new_admin: Address }, /// Emergency pause/unpause - EmergencyAction(bool), + EmergencyAction { pause: bool }, /// Custom proposal with description - Custom(Symbol, Symbol), + Custom { title: Symbol, description: Symbol }, } /// Governance proposal status @@ -123,12 +123,12 @@ pub enum ParamKey { impl Default for GovernanceConfig { fn default() -> Self { Self { - min_voting_period: 86400, // 1 day - max_voting_period: 604800, // 7 days - quorum_threshold: 2000, // 20% - approval_threshold: 5000, // 50% - execution_delay: 172800, // 2 days - proposal_cooldown: 3600, // 1 hour + min_voting_period: 86400, // 1 day + max_voting_period: 604800, // 7 days + quorum_threshold: 2000, // 20% + approval_threshold: 5000, // 50% + execution_delay: 172800, // 2 days + proposal_cooldown: 3600, // 1 hour } } -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/kyc.rs b/swaptrade-contracts/counter/src/kyc.rs index 0af5256..f4d9a67 100644 --- a/swaptrade-contracts/counter/src/kyc.rs +++ b/swaptrade-contracts/counter/src/kyc.rs @@ -280,7 +280,6 @@ impl KYCSystem { } } - // ===== KYC RECORD MANAGEMENT ===== /// Get KYC record for a user @@ -400,7 +399,7 @@ impl KYCSystem { let timestamp = env.ledger().timestamp(); let expiry_duration = Self::get_pending_expiry_duration(env); - + let mut new_record = record; new_record.status = KYCStatus::Pending; new_record.updated_at = timestamp; @@ -742,6 +741,9 @@ mod mxllv_tests { fn test_validate_helpers_are_private() { // `validate_reason` and `validate_symbol_length` are module-private (`fn`). // Attempting to call them from outside the module would fail to compile. - assert!(true, "internal validation helpers are private by construction"); + assert!( + true, + "internal validation helpers are private by construction" + ); } } diff --git a/swaptrade-contracts/counter/src/lib.rs b/swaptrade-contracts/counter/src/lib.rs index 0bb1081..6bb86c2 100644 --- a/swaptrade-contracts/counter/src/lib.rs +++ b/swaptrade-contracts/counter/src/lib.rs @@ -5,10 +5,10 @@ use soroban_sdk::{ // Bring in modules from parent directory mod admin; -mod bridge; #[cfg(test)] mod alert_tests; mod alerts; +mod bridge; mod errors; mod events; mod faucet; @@ -18,39 +18,38 @@ mod kyc; mod kyc_tests; mod liquidity_pool; mod rate_limit; +mod referral_system; mod state_snapshot; #[cfg(test)] mod state_snapshot_tests; mod storage; -mod referral_system; mod batch { include!("../batch.rs"); } mod tiers { include!("../tiers.rs"); } +#[cfg(test)] +mod analytics_dashboard_tests; #[cfg(all(test, feature = "experimental"))] mod batch_event_tests; #[cfg(all(test, feature = "experimental"))] mod batch_opt_simple_test; #[cfg(all(test, feature = "experimental"))] mod batch_performance_tests; +mod governance_params; +mod governance_system; +mod governance_types; +#[cfg(test)] +mod multihop_swap_tests; +mod nonce; mod oracle; mod oracle_adapter; -mod orders; -mod flash_loan; -#[cfg(test)] -mod orders_tests; -#[cfg(test)] -mod analytics_dashboard_tests; #[cfg(test)] mod oracle_adapter_tests; +mod orders; #[cfg(test)] -mod multihop_swap_tests; -mod governance_types; -mod governance_system; -mod governance_params; -mod nonce; +mod orders_tests; mod risk_management; pub use governance_params::{GovernanceParams, ParamKey, PendingParamUpdate}; @@ -205,6 +204,66 @@ pub fn set_admin(env: Env, caller: Address, new_admin: Address) -> Result<(), Sw Ok(()) } +pub fn set_treasury( + env: Env, + caller: Address, + new_treasury: Address, +) -> Result<(), SwapTradeError> { + caller.require_auth(); + crate::admin::require_admin(&env, &caller)?; + env.storage() + .persistent() + .set(&crate::storage::DEFAULT_TREASURY_KEY, &new_treasury); + crate::events::fee_parameters_updated(&env, 0, 0, Some(new_treasury)); + Ok(()) +} + +pub fn update_pool_fee_tier( + env: Env, + caller: Address, + pool_id: u64, + new_fee_tier: u32, +) -> Result<(), ContractError> { + caller.require_auth(); + let mut registry = load_pool_registry(&env); + registry.update_fee_tier(&env, pool_id, new_fee_tier, caller)?; + save_pool_registry(&env, ®istry); + Ok(()) +} + +pub fn claim_pool_fees( + env: Env, + caller: Address, + pool_id: u64, +) -> Result<(i128, i128), ContractError> { + caller.require_auth(); + let mut registry = load_pool_registry(&env); + let fees = registry.claim_fees(&env, pool_id, caller)?; + save_pool_registry(&env, ®istry); + Ok(fees) +} + +pub fn withdraw_treasury_fees( + env: Env, + caller: Address, + pool_id: u64, +) -> Result<(i128, i128), ContractError> { + caller.require_auth(); + // Verify caller is the treasury + let treasury: Address = env + .storage() + .persistent() + .get(&crate::storage::DEFAULT_TREASURY_KEY) + .ok_or(ContractError::InvalidAddress)?; + if caller != treasury { + return Err(ContractError::NotAuthorized); + } + let mut registry = load_pool_registry(&env); + let fees = registry.withdraw_treasury_fees(&env, pool_id, caller)?; + save_pool_registry(&env, ®istry); + Ok(fees) +} + // Batch imports use batch::{execute_batch_atomic, execute_batch_best_effort, BatchOperation, BatchResult}; @@ -368,11 +427,13 @@ impl CounterContract { } /// Swap tokens using simplified AMM (1:1 XLM <-> USDC-SIM) - pub fn swap(env: Env, from: Symbol, to: Symbol, amount: i128, min_amount_out: i128, user: Address, deadline: u64) -> Result { - if env.ledger().timestamp() > deadline { - return Err(ContractError::Expired); - } - + pub fn swap( + env: Env, + from: Symbol, + to: Symbol, + amount: i128, + user: Address, + ) -> Result { require_authenticated_verified_user(&env, &user)?; // Oracle validation @@ -468,7 +529,11 @@ impl CounterContract { portfolio.collect_fee(fee_amount); // Distribute referral commissions - crate::referral_system::calculate_and_distribute_commission(&env, user.clone(), fee_amount); + crate::referral_system::calculate_and_distribute_commission( + &env, + user.clone(), + fee_amount, + ); } let out_amount = perform_swap( @@ -780,7 +845,7 @@ impl CounterContract { // Extract caller from first operation for authentication and rate limiting let caller = match operations.get(0) { - Some(BatchOperation::Swap(_, _, _, user)) + Some(BatchOperation::Swap(_, _, _, user)) | Some(BatchOperation::AddLiquidity(_, _, user)) | Some(BatchOperation::RemoveLiquidity(_, _, user)) => Some(user.clone()), Some(BatchOperation::MintToken(_, _, _)) => None, @@ -807,7 +872,10 @@ impl CounterContract { if let Some(caller_addr) = &caller { let user_tier = portfolio.get_user_tier(&env, caller_addr.clone()); // Count swap operations in batch - let swap_count = operations.iter().filter(|op| matches!(op, BatchOperation::Swap(_, _, _, _))).count(); + let swap_count = operations + .iter() + .filter(|op| matches!(op, BatchOperation::Swap(_, _, _, _))) + .count(); if swap_count > 0 { // Apply rate limit check for batch swaps if RateLimiter::check_swap_limit(&env, caller_addr, &user_tier).is_err() { @@ -823,13 +891,20 @@ impl CounterContract { match result { Ok(res) => { env.storage().instance().set(&(), &portfolio); - + // Record rate limit usage for executed swaps if let Some(caller_addr) = &caller { - let swap_count = operations.iter().filter(|op| matches!(op, BatchOperation::Swap(_, _, _, _))).count(); + let swap_count = operations + .iter() + .filter(|op| matches!(op, BatchOperation::Swap(_, _, _, _))) + .count(); if swap_count > 0 && res.operations_executed > 0 { for _ in 0..res.operations_executed { - RateLimiter::record_swap(&env, caller_addr, env.ledger().timestamp()); + RateLimiter::record_swap_op( + &env, + caller_addr, + env.ledger().timestamp(), + ); } } } @@ -856,7 +931,7 @@ impl CounterContract { // Extract caller from first operation for authentication and rate limiting let caller = match operations.get(0) { - Some(BatchOperation::Swap(_, _, _, user)) + Some(BatchOperation::Swap(_, _, _, user)) | Some(BatchOperation::AddLiquidity(_, _, user)) | Some(BatchOperation::RemoveLiquidity(_, _, user)) => Some(user.clone()), Some(BatchOperation::MintToken(_, _, _)) => None, @@ -883,7 +958,10 @@ impl CounterContract { if let Some(caller_addr) = &caller { let user_tier = portfolio.get_user_tier(&env, caller_addr.clone()); // Count swap operations in batch - let swap_count = operations.iter().filter(|op| matches!(op, BatchOperation::Swap(_, _, _, _))).count(); + let swap_count = operations + .iter() + .filter(|op| matches!(op, BatchOperation::Swap(_, _, _, _))) + .count(); if swap_count > 0 { // Apply rate limit check for batch swaps if RateLimiter::check_swap_limit(&env, caller_addr, &user_tier).is_err() { @@ -899,13 +977,20 @@ impl CounterContract { match result { Ok(res) => { env.storage().instance().set(&(), &portfolio); - + // Record rate limit usage for executed swaps if let Some(caller_addr) = &caller { - let swap_count = operations.iter().filter(|op| matches!(op, BatchOperation::Swap(_, _, _, _))).count(); + let swap_count = operations + .iter() + .filter(|op| matches!(op, BatchOperation::Swap(_, _, _, _))) + .count(); if swap_count > 0 && res.operations_executed > 0 { for _ in 0..res.operations_executed { - RateLimiter::record_swap(&env, caller_addr, env.ledger().timestamp()); + RateLimiter::record_swap_op( + &env, + caller_addr, + env.ledger().timestamp(), + ); } } } @@ -930,7 +1015,12 @@ impl CounterContract { /// Add liquidity to the pool and mint LP tokens /// Returns the number of LP tokens minted - pub fn add_liquidity(env: Env, xlm_amount: i128, usdc_amount: i128, user: Address) -> Result { + pub fn add_liquidity( + env: Env, + xlm_amount: i128, + usdc_amount: i128, + user: Address, + ) -> Result { require_authenticated_verified_user(&env, &user)?; if xlm_amount <= 0 || usdc_amount <= 0 { @@ -1061,7 +1151,11 @@ impl CounterContract { /// Remove liquidity from the pool by burning LP tokens /// Returns (xlm_amount, usdc_amount) returned to user - pub fn remove_liquidity(env: Env, lp_tokens: i128, user: Address) -> Result<(i128, i128), ContractError> { + pub fn remove_liquidity( + env: Env, + lp_tokens: i128, + user: Address, + ) -> Result<(i128, i128), ContractError> { require_authenticated_verified_user(&env, &user)?; if lp_tokens <= 0 { @@ -1194,9 +1288,10 @@ impl CounterContract { require_verified_user(&env, &provider)?; let mut registry = load_pool_registry(&env); - let lp_tokens = registry.add_liquidity(&env, pool_id, amount_a, amount_b, provider.clone())?; + let lp_tokens = + registry.add_liquidity(&env, pool_id, amount_a, amount_b, provider.clone())?; save_pool_registry(&env, ®istry); - + // Emit LiquidityAdded event with correct signature env.events().publish( ( @@ -1206,7 +1301,7 @@ impl CounterContract { ), (amount_a, amount_b, lp_tokens, env.ledger().timestamp()), ); - + Ok(lp_tokens) } @@ -1220,9 +1315,10 @@ impl CounterContract { require_verified_user(&env, &provider)?; let mut registry = load_pool_registry(&env); - let (amount_a, amount_b) = registry.remove_liquidity(&env, pool_id, lp_tokens, provider.clone())?; + let (amount_a, amount_b) = + registry.remove_liquidity(&env, pool_id, lp_tokens, provider.clone())?; save_pool_registry(&env, ®istry); - + // Emit LiquidityRemoved event with correct signature env.events().publish( ( @@ -1232,7 +1328,7 @@ impl CounterContract { ), (amount_a, amount_b, lp_tokens, env.ledger().timestamp()), ); - + Ok((amount_a, amount_b)) } @@ -1299,7 +1395,7 @@ impl CounterContract { ) -> Result { trader.require_auth(); require_verified_user(&env, &trader)?; - + trading::execute_multihop_swap(&env, &route, amount_in, min_amount_out, &trader) } @@ -1382,7 +1478,12 @@ impl CounterContract { /// Stake tokens for a specified duration to earn bonuses /// Supports: 30, 60, 90, or 365-day stakes - pub fn stake(env: Env, user: Address, amount: i128, duration_days: u32) -> Result { + pub fn stake( + env: Env, + user: Address, + amount: i128, + duration_days: u32, + ) -> Result { require_authenticated_verified_user(&env, &user)?; let result = StakingBonusManager::stake(&env, user, amount, duration_days)?; invalidate_query_cache(&env); @@ -1409,7 +1510,11 @@ impl CounterContract { /// Unstake early before lock period (incurs 10% penalty) /// Returns (principal_after_penalty, penalty_amount) - pub fn unstake_early(env: Env, user: Address, stake_id: u32) -> Result<(i128, i128), ContractError> { + pub fn unstake_early( + env: Env, + user: Address, + stake_id: u32, + ) -> Result<(i128, i128), ContractError> { require_authenticated_verified_user(&env, &user)?; let result = StakingBonusManager::unstake_early(&env, user, stake_id)?; invalidate_query_cache(&env); @@ -1422,7 +1527,11 @@ impl CounterContract { } /// Get specific stake details - pub fn get_stake_details(env: Env, user: Address, stake_id: u32) -> Result { + pub fn get_stake_details( + env: Env, + user: Address, + stake_id: u32, + ) -> Result { StakingBonusManager::get_stake_details(&env, user, stake_id) } @@ -1466,7 +1575,15 @@ impl CounterContract { user: Address, ) -> Result { require_authenticated_verified_user(&env, &user)?; - orders::OrderManager::place_limit_order(&env, user, token_in, token_out, amount_in, limit_price, expires_at) + orders::OrderManager::place_limit_order( + &env, + user, + token_in, + token_out, + amount_in, + limit_price, + expires_at, + ) } /// Place a stop-loss order that executes when price reaches trigger_price @@ -1480,7 +1597,15 @@ impl CounterContract { user: Address, ) -> Result { require_authenticated_verified_user(&env, &user)?; - orders::OrderManager::place_stop_loss(&env, user, token_in, token_out, amount_in, trigger_price, expires_at) + orders::OrderManager::place_stop_loss( + &env, + user, + token_in, + token_out, + amount_in, + trigger_price, + expires_at, + ) } /// Cancel an existing order @@ -1566,12 +1691,20 @@ impl CounterContract { // ──────────────────────────────────────────────────────────────────────── /// Add a KYC operator (admin only) - pub fn kyc_add_operator(env: Env, admin: Address, operator: Address) -> Result<(), ContractError> { + pub fn kyc_add_operator( + env: Env, + admin: Address, + operator: Address, + ) -> Result<(), ContractError> { kyc::KYCSystem::add_operator(&env, &admin, operator) } /// Remove a KYC operator (admin only) - pub fn kyc_remove_operator(env: Env, admin: Address, operator: Address) -> Result<(), ContractError> { + pub fn kyc_remove_operator( + env: Env, + admin: Address, + operator: Address, + ) -> Result<(), ContractError> { kyc::KYCSystem::remove_operator(&env, &admin, operator) } @@ -1612,7 +1745,11 @@ impl CounterContract { } /// Set timelock duration for governance overrides (admin only) - pub fn kyc_set_timelock_duration(env: Env, admin: Address, duration: u64) -> Result<(), ContractError> { + pub fn kyc_set_timelock_duration( + env: Env, + admin: Address, + duration: u64, + ) -> Result<(), ContractError> { kyc::KYCSystem::set_timelock_duration(&env, &admin, duration) } @@ -1622,7 +1759,11 @@ impl CounterContract { } /// Set pending KYC expiry duration (admin only) - pub fn kyc_set_pending_expiry_duration(env: Env, admin: Address, duration: u64) -> Result<(), ContractError> { + pub fn kyc_set_pending_expiry_duration( + env: Env, + admin: Address, + duration: u64, + ) -> Result<(), ContractError> { kyc::KYCSystem::set_pending_expiry_duration(&env, &admin, duration) } @@ -1643,7 +1784,11 @@ impl CounterContract { } /// Execute governance override after timelock (admin only) - pub fn kyc_execute_override(env: Env, admin: Address, override_id: u64) -> Result<(), ContractError> { + pub fn kyc_execute_override( + env: Env, + admin: Address, + override_id: u64, + ) -> Result<(), ContractError> { kyc::KYCSystem::execute_override(&env, &admin, override_id) } @@ -1655,7 +1800,11 @@ impl CounterContract { // ── Referral System ───────────────────────────────────────────────────── /// Register a referral relationship - pub fn register_referral(env: Env, referrer: Address, referred: Address) -> Result<(), ContractError> { + pub fn register_referral( + env: Env, + referrer: Address, + referred: Address, + ) -> Result<(), ContractError> { referral_system::register_referral(&env, referrer, referred) } @@ -1824,13 +1973,8 @@ impl CounterContract { } } +mod governance_tests; #[cfg(all(test, feature = "experimental"))] mod migration_tests; #[cfg(test)] mod risk_management_tests; -#[cfg(test)] -mod governance_tests; -#[cfg(test)] -mod referral_system_tests; -#[cfg(test)] -mod referral_integration_test; \ No newline at end of file diff --git a/swaptrade-contracts/counter/src/liquidity_pool.rs b/swaptrade-contracts/counter/src/liquidity_pool.rs index 06755e3..cf5bed9 100644 --- a/swaptrade-contracts/counter/src/liquidity_pool.rs +++ b/swaptrade-contracts/counter/src/liquidity_pool.rs @@ -15,6 +15,8 @@ pub struct LiquidityPool { pub reserve_b: i128, pub total_lp_tokens: i128, pub fee_tier: u32, + pub accumulated_fees_a: i128, + pub accumulated_fees_b: i128, } #[derive(Clone, Debug, PartialEq)] @@ -112,6 +114,8 @@ impl PoolRegistry { reserve_b, total_lp_tokens: initial_lp, fee_tier, + accumulated_fees_a: 0, + accumulated_fees_b: 0, }, ); self.pair_to_pool.set((norm_a, norm_b), pool_id); @@ -182,6 +186,100 @@ impl PoolRegistry { Ok(lp_tokens) } + pub fn claim_fees( + &mut self, + env: &Env, + pool_id: u64, + provider: Address, + ) -> Result<(i128, i128), ContractError> { + let pool = self + .pools + .get(pool_id) + .ok_or(ContractError::LPPositionNotFound)?; + let key = (pool_id, provider.clone()); + let lp_balance = self.lp_balances.get(key.clone()).unwrap_or(0); + if lp_balance <= 0 { + return Err(ContractError::InsufficientLPTokens); + } + + // Calculate proportional share of accumulated fees + let share = (lp_balance as u128) / (pool.total_lp_tokens as u128); + let fees_a = ((pool.accumulated_fees_a as u128) * share) as i128; + let fees_b = ((pool.accumulated_fees_b as u128) * share) as i128; + + // Update accumulated fees (subtract claimed amount) + let mut updated_pool = pool.clone(); + updated_pool.accumulated_fees_a = pool.accumulated_fees_a - fees_a; + updated_pool.accumulated_fees_b = pool.accumulated_fees_b - fees_b; + self.pools.set(pool_id, updated_pool); + + // Publish events + if fees_a > 0 { + crate::events::fees_distributed(env, pool_id, pool.token_a, fees_a, provider.clone()); + } + if fees_b > 0 { + crate::events::fees_distributed(env, pool_id, pool.token_b, fees_b, provider); + } + + Ok((fees_a, fees_b)) + } + + pub fn withdraw_treasury_fees( + &mut self, + env: &Env, + pool_id: u64, + treasury: Address, + ) -> Result<(i128, i128), ContractError> { + treasury.require_auth(); + let mut pool = self + .pools + .get(pool_id) + .ok_or(ContractError::LPPositionNotFound)?; + + let fees_a = pool.accumulated_fees_a; + let fees_b = pool.accumulated_fees_b; + + // Reset accumulated fees + pool.accumulated_fees_a = 0; + pool.accumulated_fees_b = 0; + self.pools.set(pool_id, pool); + + // Publish events + if fees_a > 0 { + crate::events::fees_distributed(env, pool_id, pool.token_a, fees_a, treasury.clone()); + } + if fees_b > 0 { + crate::events::fees_distributed(env, pool_id, pool.token_b, fees_b, treasury); + } + + Ok((fees_a, fees_b)) + } + + pub fn update_fee_tier( + &mut self, + env: &Env, + pool_id: u64, + new_fee_tier: u32, + admin: Address, + ) -> Result<(), ContractError> { + admin.require_auth(); + crate::admin::require_admin(env, &admin)?; + + if ![1, 5, 30].contains(&new_fee_tier) { + return Err(ContractError::InvalidAmount); + } + + let mut pool = self + .pools + .get(pool_id) + .ok_or(ContractError::LPPositionNotFound)?; + pool.fee_tier = new_fee_tier; + self.pools.set(pool_id, pool); + + crate::events::fee_parameters_updated(env, pool_id, new_fee_tier, None); + Ok(()) + } + pub fn remove_liquidity( &mut self, env: &Env, @@ -189,6 +287,9 @@ impl PoolRegistry { lp_tokens: i128, provider: Address, ) -> Result<(i128, i128), ContractError> { + // First claim any pending fees before removing liquidity + self.claim_fees(env, pool_id, provider.clone())?; + let mut pool = self .pools .get(pool_id) @@ -246,18 +347,19 @@ impl PoolRegistry { return Err(ContractError::InvalidAmount); } - let (reserve_in, reserve_out) = if token_in == pool.token_a { - (pool.reserve_a, pool.reserve_b) + let (reserve_in, reserve_out, is_token_a) = if token_in == pool.token_a { + (pool.reserve_a, pool.reserve_b, true) } else if token_in == pool.token_b { - (pool.reserve_b, pool.reserve_a) + (pool.reserve_b, pool.reserve_a, false) } else { return Err(ContractError::InvalidTokenSymbol); }; - let amount_in_with_fee = (amount_in as u128) - .checked_mul(10000 - pool.fee_tier as u128) - .ok_or(ContractError::AmountOverflow)? - / 10000; + // Calculate fee and amount after fee + let fee_amount = ((amount_in as u128) * (pool.fee_tier as u128) / 10000) as i128; + let amount_in_after_fee = amount_in - fee_amount; + + let amount_in_with_fee = amount_in_after_fee as u128; let numerator = (reserve_out as u128) .checked_mul(amount_in_with_fee) .ok_or(ContractError::AmountOverflow)?; @@ -270,25 +372,38 @@ impl PoolRegistry { return Err(ContractError::SlippageExceeded); } - if token_in == pool.token_a { + // Accumulate fees + if is_token_a { + pool.accumulated_fees_a = pool + .accumulated_fees_a + .checked_add(fee_amount) + .ok_or(ContractError::AmountOverflow)?; pool.reserve_a = pool .reserve_a - .checked_add(amount_in) + .checked_add(amount_in_after_fee) .ok_or(ContractError::AmountOverflow)?; pool.reserve_b = pool .reserve_b .checked_sub(amount_out) .ok_or(ContractError::InsufficientBalance)?; } else { + pool.accumulated_fees_b = pool + .accumulated_fees_b + .checked_add(fee_amount) + .ok_or(ContractError::AmountOverflow)?; pool.reserve_b = pool .reserve_b - .checked_add(amount_in) + .checked_add(amount_in_after_fee) .ok_or(ContractError::AmountOverflow)?; pool.reserve_a = pool .reserve_a .checked_sub(amount_out) .ok_or(ContractError::InsufficientBalance)?; } + + // Publish fees collected event + crate::events::fees_collected(env, token_in, fee_amount, pool_id); + self.pools.set(pool_id, pool); Ok(amount_out) } @@ -338,9 +453,9 @@ impl PoolRegistry { if let Some(pool2_id) = self.pair_to_pool.get((norm_int, norm_out)) { if let Some(pool2) = self.pools.get(pool2_id) { let out1 = - self.calculate_output(&pool1, token_in.clone(), amount_in).ok()?; + self.calculate_output(&pool1, token_in.clone(), amount_in)?; let out2 = - self.calculate_output(&pool2, intermediate.clone(), out1).ok()?; + self.calculate_output(&pool2, intermediate.clone(), out1)?; let impact1 = self.calculate_price_impact( &pool1, token_in.clone(), @@ -375,31 +490,12 @@ impl PoolRegistry { best_route } - pub fn simulate_route( + fn calculate_output( &self, - route: &Route, + pool: &LiquidityPool, + token_in: Symbol, amount_in: i128, - ) -> Option<(i128, u32)> { - if route.pools.len() == 0 || route.tokens.len() < 2 { - return None; - } - let mut current_amount = amount_in; - let mut total_fees_bps: u32 = 0; - - for idx in 0..route.pools.len() { - let pool_id = route.pools.get(idx)?; - let pool = self.pools.get(pool_id)?; - let token_in = route.tokens.get(idx)?; - - let output = self.calculate_output(&pool, token_in, current_amount).ok()?; - total_fees_bps = total_fees_bps.saturating_add(pool.fee_tier); - current_amount = output; - } - - Some((current_amount, total_fees_bps)) - } - - fn calculate_output(&self, pool: &LiquidityPool, token_in: Symbol, amount_in: i128) -> Result { + ) -> Result { let (reserve_in, reserve_out) = if token_in == pool.token_a { (pool.reserve_a, pool.reserve_b) } else { diff --git a/swaptrade-contracts/counter/src/multihop_swap_tests.rs b/swaptrade-contracts/counter/src/multihop_swap_tests.rs index 243b0e3..d795d76 100644 --- a/swaptrade-contracts/counter/src/multihop_swap_tests.rs +++ b/swaptrade-contracts/counter/src/multihop_swap_tests.rs @@ -50,7 +50,7 @@ fn test_two_hop_swap_execution() { // Execute multi-hop swap let min_out = ((r.expected_output as u128).saturating_mul(9500) / 10000) as i128; // 5% slippage let result = client.execute_multi_hop_swap(&r, &100, &min_out, &trader); - + assert!(result > 0); } @@ -81,7 +81,7 @@ fn test_multi_hop_respects_slippage_tolerance() { // Try to execute with very tight slippage (should fail) let tight_min_out = r.expected_output + 1000; // Unrealistic expectation let result = client.try_execute_multi_hop_swap(&r, &500, &tight_min_out, &trader); - + // Should fail due to slippage assert!(result.is_err()); } @@ -106,7 +106,7 @@ fn test_multi_hop_atomic_execution() { // Try to find route (should not find 2-hop route) let route = client.find_best_route(&xlm, &btc, &100); - + // Route should be None since second pool doesn't exist if route.is_none() { // Test passes - route discovery prevents invalid execution @@ -116,7 +116,7 @@ fn test_multi_hop_atomic_execution() { // If route exists but pool is invalid, execution should fail let r = route.unwrap(); let result = client.try_execute_multi_hop_swap(&r, &100, &0, &trader); - + // Should fail due to missing pool assert!(result.is_err()); } @@ -178,7 +178,7 @@ fn test_single_hop_swap_via_route() { // Execute via multi-hop function (should work for single hop too) let min_out = ((r.expected_output as u128).saturating_mul(9500) / 10000) as i128; let result = client.execute_multi_hop_swap(&r, &100, &min_out, &trader); - + assert!(result > 0); } @@ -252,7 +252,7 @@ fn test_three_hop_route_execution() { // Find route from XLM to ETH let route = client.find_best_route(&xlm, ð, &100); - + // Note: Current find_best_route only supports up to 2 hops // This test validates that the execution function can handle it if route is provided if route.is_some() { diff --git a/swaptrade-contracts/counter/src/nft_lending.rs b/swaptrade-contracts/counter/src/nft_lending.rs index 14ad03f..80d8710 100644 --- a/swaptrade-contracts/counter/src/nft_lending.rs +++ b/swaptrade-contracts/counter/src/nft_lending.rs @@ -211,7 +211,8 @@ pub fn fund_loan(env: &Env, lender: Address, loan_id: u64) -> Result<(), NFTErro let current_time = env.ledger().timestamp(); // Calculate repayment amount using scaled arithmetic to prevent precision loss - let scaled_principal = (loan.loan_amount as u128).checked_mul(INTEREST_PRECISION) + let scaled_principal = (loan.loan_amount as u128) + .checked_mul(INTEREST_PRECISION) .ok_or(NFTError::InterestOverflow)?; let daily_interest_rate = loan.interest_rate_bps as u128; let daily_interest_scaled = scaled_principal @@ -224,7 +225,8 @@ pub fn fund_loan(env: &Env, lender: Address, loan_id: u64) -> Result<(), NFTErro .checked_mul(days) .ok_or(NFTError::InterestOverflow)?; let total_interest = (total_interest_scaled / INTEREST_PRECISION) as i128; - loan.repayment_amount = loan.loan_amount + loan.repayment_amount = loan + .loan_amount .checked_add(total_interest as i128) .ok_or(NFTError::AmountOverflow)?; diff --git a/swaptrade-contracts/counter/src/nft_types.rs b/swaptrade-contracts/counter/src/nft_types.rs index 1294f93..04ce9ff 100644 --- a/swaptrade-contracts/counter/src/nft_types.rs +++ b/swaptrade-contracts/counter/src/nft_types.rs @@ -529,7 +529,8 @@ impl NFTLoan { let days_elapsed = elapsed / 86400; // Use saturating arithmetic to prevent overflow // Scale to u128 for precision, then convert back - let scaled_principal = (self.loan_amount as u128).saturating_mul(1_000_000_000_000_000_000u128); + let scaled_principal = + (self.loan_amount as u128).saturating_mul(1_000_000_000_000_000_000u128); let daily_interest_rate = self.interest_rate_bps as u128; let daily_interest_scaled = scaled_principal .saturating_mul(daily_interest_rate) diff --git a/swaptrade-contracts/counter/src/oracle_adapter.rs b/swaptrade-contracts/counter/src/oracle_adapter.rs index f7db22d..9bc4bd4 100644 --- a/swaptrade-contracts/counter/src/oracle_adapter.rs +++ b/swaptrade-contracts/counter/src/oracle_adapter.rs @@ -91,8 +91,12 @@ impl OracleAdapter { fallback_price: initial_price, }; - env.storage().instance().set(&Self::config_key(&pair), &config); - env.storage().instance().set(&Self::state_key(&pair), &state); + env.storage() + .instance() + .set(&Self::config_key(&pair), &config); + env.storage() + .instance() + .set(&Self::state_key(&pair), &state); Ok(()) } @@ -100,7 +104,7 @@ impl OracleAdapter { /// Get price with TWAP validation and staleness checks pub fn get_price(env: &Env, pair: (Symbol, Symbol)) -> Result { let config = Self::get_config(env, &pair)?; - + if !config.is_active { return Err(ContractError::OracleNotActive); } @@ -153,15 +157,17 @@ impl OracleAdapter { // Circuit breaker check: reject if deviation > threshold if state.current_price > 0 { let deviation_bps = Self::calculate_deviation_bps(state.current_price, new_price); - + if deviation_bps > config.circuit_breaker_threshold_bps { // Activate circuit breaker state.circuit_breaker_active = true; state.fallback_price = state.current_price; - + // Store updated state - env.storage().instance().set(&Self::state_key(&pair), &state); - + env.storage() + .instance() + .set(&Self::state_key(&pair), &state); + return Err(ContractError::CircuitBreakerTriggered); } } @@ -182,7 +188,7 @@ impl OracleAdapter { // Update state state.current_price = new_price; state.last_update = current_time; - + // Deactivate circuit breaker if price is stable if state.circuit_breaker_active { let deviation_bps = Self::calculate_deviation_bps(state.fallback_price, new_price); @@ -192,7 +198,9 @@ impl OracleAdapter { } } - env.storage().instance().set(&Self::state_key(&pair), &state); + env.storage() + .instance() + .set(&Self::state_key(&pair), &state); Ok(()) } @@ -285,15 +293,23 @@ impl OracleAdapter { } } - env.storage().instance().set(&Self::config_key(&pair), &config); + env.storage() + .instance() + .set(&Self::config_key(&pair), &config); Ok(()) } /// Activate or deactivate oracle - pub fn set_oracle_active(env: &Env, pair: (Symbol, Symbol), active: bool) -> Result<(), ContractError> { + pub fn set_oracle_active( + env: &Env, + pair: (Symbol, Symbol), + active: bool, + ) -> Result<(), ContractError> { let mut config = Self::get_config(env, &pair)?; config.is_active = active; - env.storage().instance().set(&Self::config_key(&pair), &config); + env.storage() + .instance() + .set(&Self::config_key(&pair), &config); Ok(()) } @@ -301,12 +317,17 @@ impl OracleAdapter { pub fn reset_circuit_breaker(env: &Env, pair: (Symbol, Symbol)) -> Result<(), ContractError> { let mut state = Self::get_state(env, &pair)?; state.circuit_breaker_active = false; - env.storage().instance().set(&Self::state_key(&pair), &state); + env.storage() + .instance() + .set(&Self::state_key(&pair), &state); Ok(()) } /// Get oracle state information - pub fn get_oracle_info(env: &Env, pair: (Symbol, Symbol)) -> Result<(OracleConfig, OracleState), ContractError> { + pub fn get_oracle_info( + env: &Env, + pair: (Symbol, Symbol), + ) -> Result<(OracleConfig, OracleState), ContractError> { let config = Self::get_config(env, &pair)?; let state = Self::get_state(env, &pair)?; Ok((config, state)) diff --git a/swaptrade-contracts/counter/src/oracle_adapter_tests.rs b/swaptrade-contracts/counter/src/oracle_adapter_tests.rs index 8599d86..ce358e5 100644 --- a/swaptrade-contracts/counter/src/oracle_adapter_tests.rs +++ b/swaptrade-contracts/counter/src/oracle_adapter_tests.rs @@ -23,7 +23,7 @@ fn test_initialize_oracle() { setup_oracle(&env, pair.clone()); let (config, state) = OracleAdapter::get_oracle_info(&env, pair).unwrap(); - + assert!(config.is_active); assert_eq!(config.staleness_threshold, 300); assert_eq!(config.circuit_breaker_threshold_bps, 1000); @@ -63,7 +63,7 @@ fn test_circuit_breaker_triggers_on_large_deviation() { // Try to update price by 15% (exceeds 10% threshold) let new_price = (PRECISION as u128).saturating_mul(11_500) / 10_000; let result = OracleAdapter::update_price(&env, pair.clone(), new_price); - + assert!(result.is_err()); assert_eq!(result.unwrap_err(), ContractError::CircuitBreakerTriggered); @@ -135,7 +135,7 @@ fn test_twap_calculation() { // Advance time for each observation let mut ledger = env.ledger(); ledger.set_timestamp(env.ledger().timestamp() + 10); - + if i == 0 { // First update is already done in setup continue; @@ -145,11 +145,11 @@ fn test_twap_calculation() { // Get price should return TWAP let twap_price = OracleAdapter::get_price(&env, pair).unwrap(); - + // TWAP should be average of all observations // Expected average is approximately 5.4% increase let expected_avg = PRECISION.saturating_mul(10_540) / 10_000; - + // Allow small rounding difference assert!(twap_price >= expected_avg - 1_000_000); assert!(twap_price <= expected_avg + 1_000_000); @@ -168,13 +168,14 @@ fn test_update_config() { OracleAdapter::update_config( &env, pair.clone(), - Some(600), // 10 minutes staleness - Some(500), // 5% circuit breaker - Some(20), // TWAP window of 20 - ).unwrap(); + Some(600), // 10 minutes staleness + Some(500), // 5% circuit breaker + Some(20), // TWAP window of 20 + ) + .unwrap(); let (config, _) = OracleAdapter::get_oracle_info(&env, pair).unwrap(); - + assert_eq!(config.staleness_threshold, 600); assert_eq!(config.circuit_breaker_threshold_bps, 500); assert_eq!(config.twap_window_size, 20); @@ -190,14 +191,8 @@ fn test_invalid_config_rejected() { setup_oracle(&env, pair.clone()); // Try to set invalid TWAP window size (0) - let result = OracleAdapter::update_config( - &env, - pair.clone(), - None, - None, - Some(0), - ); - + let result = OracleAdapter::update_config(&env, pair.clone(), None, None, Some(0)); + assert!(result.is_err()); assert_eq!(result.unwrap_err(), ContractError::InvalidConfig); } @@ -288,7 +283,7 @@ fn test_circuit_breaker_deactivates_on_stable_price() { #[test] fn test_deviation_calculation() { let old_price = PRECISION; - + // 10% deviation = 1000 bps let new_price_10pct = (PRECISION as u128).saturating_mul(11_000) / 10_000; let deviation = OracleAdapter::calculate_deviation_bps(old_price, new_price_10pct); diff --git a/swaptrade-contracts/counter/src/orders.rs b/swaptrade-contracts/counter/src/orders.rs index afe3d32..1605fee 100644 --- a/swaptrade-contracts/counter/src/orders.rs +++ b/swaptrade-contracts/counter/src/orders.rs @@ -6,21 +6,20 @@ use crate::errors::ContractError; #[contracttype] #[derive(Clone, Debug, PartialEq)] pub enum OrderType { - Market, // Execute immediately at best available price - Limit, // Execute only at specified price or better - StopLoss, // Execute when price reaches trigger (becomes market order) - StopLimit, // Execute when price reaches trigger (becomes limit order) - Recurring, // DCA / recurring order executed on a schedule + Market, // Execute immediately at best available price + Limit, // Execute only at specified price or better + StopLoss, // Execute when price reaches trigger (becomes market order) + StopLimit, // Execute when price reaches trigger (becomes limit order) } /// Order status #[contracttype] #[derive(Clone, Debug, PartialEq)] pub enum OrderStatus { - Pending, // Order is active and waiting to be filled - Filled, // Order has been completely filled - Cancelled, // Order was cancelled by user - Expired, // Order expired without being filled + Pending, // Order is active and waiting to be filled + Filled, // Order has been completely filled + Cancelled, // Order was cancelled by user + Expired, // Order expired without being filled PartiallyFilled, // Order is partially executed Scheduled, // Recurring order is between executions, waiting for next run } @@ -36,11 +35,11 @@ pub struct Order { pub token_out: Symbol, pub amount_in: i128, pub amount_filled: i128, - pub limit_price: Option, // For limit orders: minimum acceptable price - pub trigger_price: Option, // For stop orders: price that triggers execution + pub limit_price: Option, // For limit orders: minimum acceptable price + pub trigger_price: Option, // For stop orders: price that triggers execution pub status: OrderStatus, pub created_at: u64, - pub expires_at: Option, // None means no expiry + pub expires_at: Option, // None means no expiry pub filled_at: Option, pub interval_secs: Option, // For recurring: seconds between executions pub remaining_occurrences: Option, // For recurring: how many more times to execute @@ -52,8 +51,8 @@ pub struct Order { #[derive(Clone, Debug, PartialEq)] pub struct OrderBook { pub token_pair: (Symbol, Symbol), - pub buy_orders: Vec, // Order IDs for buy orders - pub sell_orders: Vec, // Order IDs for sell orders + pub buy_orders: Vec, // Order IDs for buy orders + pub sell_orders: Vec, // Order IDs for sell orders } /// Order manager - handles order lifecycle @@ -164,10 +163,10 @@ impl OrderManager { ) -> Result, ContractError> { let mut executed_orders = Vec::new(env); let pair_key = Self::order_book_key(&(token_in.clone(), token_out.clone())); - + // Get order book for this pair let order_book: Option = env.storage().instance().get(&pair_key); - + if order_book.is_none() { return Ok(executed_orders); } @@ -189,7 +188,9 @@ impl OrderManager { } // Check if order can be executed - if order.status == OrderStatus::Pending || order.status == OrderStatus::PartiallyFilled { + if order.status == OrderStatus::Pending + || order.status == OrderStatus::PartiallyFilled + { let should_execute = match order.order_type { OrderType::Limit => { // Execute if current price is at or below limit @@ -220,7 +221,12 @@ impl OrderManager { // Emit execution event env.events().publish( (symbol_short!("ofill"), order_id), - (order.owner, token_in.clone(), token_out.clone(), current_price), + ( + order.owner, + token_in.clone(), + token_out.clone(), + current_price, + ), ); } } @@ -243,13 +249,16 @@ impl OrderManager { /// Get user's active orders pub fn get_user_orders(env: &Env, user: Address) -> Vec { let mut orders = Vec::new(env); - let user_order_ids: Option> = env.storage().instance().get(&Self::user_orders_key(&user)); + let user_order_ids: Option> = + env.storage().instance().get(&Self::user_orders_key(&user)); if let Some(order_ids) = user_order_ids { for i in 0..order_ids.len() { if let Some(order_id) = order_ids.get(i) { if let Ok(order) = Self::get_order(env, order_id) { - if order.status == OrderStatus::Pending || order.status == OrderStatus::PartiallyFilled || order.status == OrderStatus::Scheduled { + if order.status == OrderStatus::Pending + || order.status == OrderStatus::PartiallyFilled + { orders.push_back(order); } } @@ -273,8 +282,12 @@ impl OrderManager { expires_at: Option, ) -> Result { // Generate order ID - let next_id: u64 = env.storage().instance().get(&symbol_short!("next_oid")).unwrap_or(1); - + let next_id: u64 = env + .storage() + .instance() + .get(&symbol_short!("next_oid")) + .unwrap_or(1); + let order = Order { order_id: next_id, owner: owner.clone(), @@ -298,23 +311,36 @@ impl OrderManager { Self::save_order(env, &order); // Add to user's order list - let mut user_orders: Vec = env.storage() + let mut user_orders: Vec = env + .storage() .instance() .get(&Self::user_orders_key(&owner)) .unwrap_or_else(|| Vec::new(env)); user_orders.push_back(next_id); - env.storage().instance().set(&Self::user_orders_key(&owner), &user_orders); + env.storage() + .instance() + .set(&Self::user_orders_key(&owner), &user_orders); // Add to order book Self::add_to_order_book(env, token_in.clone(), token_out.clone(), next_id); // Increment next order ID - env.storage().instance().set(&symbol_short!("next_oid"), &(next_id + 1)); + env.storage() + .instance() + .set(&symbol_short!("next_oid"), &(next_id + 1)); // Emit order placement event env.events().publish( (symbol_short!("order_new"), next_id), - (owner, order_type, token_in, token_out, amount_in, limit_price, trigger_price), + ( + owner, + order_type, + token_in, + token_out, + amount_in, + limit_price, + trigger_price, + ), ); Ok(next_id) @@ -494,8 +520,9 @@ impl OrderManager { fn add_to_order_book(env: &Env, token_in: Symbol, token_out: Symbol, order_id: u64) { let pair = (token_in.clone(), token_out.clone()); let pair_key = Self::order_book_key(&pair); - - let mut book: OrderBook = env.storage() + + let mut book: OrderBook = env + .storage() .instance() .get(&pair_key) .unwrap_or(OrderBook { diff --git a/swaptrade-contracts/counter/src/orders_tests.rs b/swaptrade-contracts/counter/src/orders_tests.rs index e099914..95256bd 100644 --- a/swaptrade-contracts/counter/src/orders_tests.rs +++ b/swaptrade-contracts/counter/src/orders_tests.rs @@ -24,7 +24,8 @@ fn test_place_limit_order() { 1000, PRECISION, // 1:1 price None, // No expiry - ).unwrap(); + ) + .unwrap(); assert_eq!(order_id, 1); @@ -57,7 +58,8 @@ fn test_place_stop_loss() { 500, trigger_price, None, - ).unwrap(); + ) + .unwrap(); assert_eq!(order_id, 1); @@ -75,15 +77,9 @@ fn test_cancel_order() { let usdc = symbol_short!("USDC"); // Place order - let order_id = OrderManager::place_limit_order( - &env, - user.clone(), - xlm, - usdc, - 1000, - PRECISION, - None, - ).unwrap(); + let order_id = + OrderManager::place_limit_order(&env, user.clone(), xlm, usdc, 1000, PRECISION, None) + .unwrap(); // Cancel order OrderManager::cancel_order(&env, order_id, user.clone()).unwrap(); @@ -102,15 +98,9 @@ fn test_cancel_order_wrong_owner() { let usdc = symbol_short!("USDC"); // Place order with user1 - let order_id = OrderManager::place_limit_order( - &env, - user1.clone(), - xlm, - usdc, - 1000, - PRECISION, - None, - ).unwrap(); + let order_id = + OrderManager::place_limit_order(&env, user1.clone(), xlm, usdc, 1000, PRECISION, None) + .unwrap(); // Try to cancel with user2 (should fail) let result = OrderManager::cancel_order(&env, order_id, user2); @@ -125,8 +115,26 @@ fn test_get_user_orders() { let usdc = symbol_short!("USDC"); // Place multiple orders - OrderManager::place_limit_order(&env, user.clone(), xlm.clone(), usdc.clone(), 1000, PRECISION, None).unwrap(); - OrderManager::place_stop_loss(&env, user.clone(), xlm.clone(), usdc.clone(), 500, PRECISION, None).unwrap(); + OrderManager::place_limit_order( + &env, + user.clone(), + xlm.clone(), + usdc.clone(), + 1000, + PRECISION, + None, + ) + .unwrap(); + OrderManager::place_stop_loss( + &env, + user.clone(), + xlm.clone(), + usdc.clone(), + 500, + PRECISION, + None, + ) + .unwrap(); // Get user orders let orders = OrderManager::get_user_orders(&env, user); @@ -150,7 +158,8 @@ fn test_order_with_expiry() { 1000, PRECISION, Some(expiry), - ).unwrap(); + ) + .unwrap(); let order = OrderManager::get_order(&env, order_id).unwrap(); assert_eq!(order.expires_at, Some(expiry)); @@ -176,15 +185,8 @@ fn test_invalid_order_amount() { assert!(result.is_err()); // Try to place order with negative amount - let result = OrderManager::place_stop_loss( - &env, - user.clone(), - xlm, - usdc, - -100, - PRECISION, - None, - ); + let result = + OrderManager::place_stop_loss(&env, user.clone(), xlm, usdc, -100, PRECISION, None); assert!(result.is_err()); } @@ -208,15 +210,7 @@ fn test_invalid_order_price() { assert!(result.is_err()); // Try to place stop-loss with zero trigger - let result = OrderManager::place_stop_loss( - &env, - user, - xlm, - usdc, - 500, - 0, - None, - ); + let result = OrderManager::place_stop_loss(&env, user, xlm, usdc, 500, 0, None); assert!(result.is_err()); } @@ -228,9 +222,36 @@ fn test_order_id_increment() { let usdc = symbol_short!("USDC"); // Place multiple orders - let id1 = OrderManager::place_limit_order(&env, user.clone(), xlm.clone(), usdc.clone(), 100, PRECISION, None).unwrap(); - let id2 = OrderManager::place_limit_order(&env, user.clone(), xlm.clone(), usdc.clone(), 200, PRECISION, None).unwrap(); - let id3 = OrderManager::place_stop_loss(&env, user.clone(), xlm.clone(), usdc.clone(), 300, PRECISION, None).unwrap(); + let id1 = OrderManager::place_limit_order( + &env, + user.clone(), + xlm.clone(), + usdc.clone(), + 100, + PRECISION, + None, + ) + .unwrap(); + let id2 = OrderManager::place_limit_order( + &env, + user.clone(), + xlm.clone(), + usdc.clone(), + 200, + PRECISION, + None, + ) + .unwrap(); + let id3 = OrderManager::place_stop_loss( + &env, + user.clone(), + xlm.clone(), + usdc.clone(), + 300, + PRECISION, + None, + ) + .unwrap(); assert_eq!(id1, 1); assert_eq!(id2, 2); @@ -253,15 +274,17 @@ fn test_match_pending_orders() { 1000, PRECISION, None, - ).unwrap(); + ) + .unwrap(); // Match orders with current price at or below limit let current_price = (PRECISION as u128).saturating_mul(9_900) / 10_000; // 1% below limit - let executed = OrderManager::match_pending_orders(&env, xlm.clone(), usdc.clone(), current_price).unwrap(); + let executed = + OrderManager::match_pending_orders(&env, xlm.clone(), usdc.clone(), current_price).unwrap(); // Order should be executed assert!(executed.len() > 0); - + let order = OrderManager::get_order(&env, order_id).unwrap(); assert_eq!(order.status, OrderStatus::Filled); } diff --git a/swaptrade-contracts/counter/src/referral_integration_test.rs b/swaptrade-contracts/counter/src/referral_integration_test.rs index a43eded..939ad28 100644 --- a/swaptrade-contracts/counter/src/referral_integration_test.rs +++ b/swaptrade-contracts/counter/src/referral_integration_test.rs @@ -1,8 +1,9 @@ #[cfg(test)] mod integration_tests { -use soroban_sdk::{Env, Address}; - use soroban_sdk::testutils::{Address as _, Ledger as _}; + use super::*; use crate::CounterContract; + use soroban_sdk::testutils::{Address as _, Ledger as _}; + use soroban_sdk::Address; #[test] fn test_referral_integration_with_swap() { @@ -10,12 +11,15 @@ use soroban_sdk::{Env, Address}; let admin = Address::generate(&env); let referrer = Address::generate(&env); let trader = Address::generate(&env); - + // Initialize contract CounterContract::initialize(env.clone()); // Register referral - assert!(CounterContract::register_referral(env.clone(), referrer.clone(), trader.clone()).is_ok()); + assert!( + CounterContract::register_referral(env.clone(), referrer.clone(), trader.clone()) + .is_ok() + ); // Check initial stats let stats = CounterContract::get_referral_stats(env.clone(), referrer.clone()); @@ -24,9 +28,10 @@ use soroban_sdk::{Env, Address}; // Set up some basic liquidity for swap (this would normally be done through liquidity pools) // For this test, we'll just verify the referral system integration points - + // Verify commission balance starts at zero - let commission_balance = CounterContract::get_commission_balance(env.clone(), referrer.clone()); + let commission_balance = + CounterContract::get_commission_balance(env.clone(), referrer.clone()); assert_eq!(commission_balance, 0); // The actual swap integration would be tested in the full integration test suite @@ -38,7 +43,7 @@ use soroban_sdk::{Env, Address}; let env = Env::default(); let admin = Address::generate(&env); let user = Address::generate(&env); - + // Initialize contract CounterContract::initialize(env.clone()); diff --git a/swaptrade-contracts/counter/src/referral_system.rs b/swaptrade-contracts/counter/src/referral_system.rs index 57ea3e5..a2f53fd 100644 --- a/swaptrade-contracts/counter/src/referral_system.rs +++ b/swaptrade-contracts/counter/src/referral_system.rs @@ -1,6 +1,6 @@ -use soroban_sdk::{contracttype, Address, Env, Map, Vec, Symbol, symbol_short}; -use crate::storage::DataKey; use crate::errors::SwapTradeError; +use crate::storage::DataKey; +use soroban_sdk::{contracttype, symbol_short, Address, Env, Map, Symbol, Vec}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -30,7 +30,7 @@ pub struct ReferralStats { #[derive(Clone, Debug, Eq, PartialEq)] pub struct TierConfig { pub volume_threshold: i128, - pub direct_commission_bps: u32, // basis points (1/100 of percent) + pub direct_commission_bps: u32, // basis points (1/100 of percent) pub indirect_commission_bps: u32, } @@ -53,7 +53,11 @@ const DEFAULT_TIER_3: TierConfig = TierConfig { indirect_commission_bps: 40, // 0.4% }; -pub fn register_referral(env: &Env, referrer: Address, referred: Address) -> Result<(), SwapTradeError> { +pub fn register_referral( + env: &Env, + referrer: Address, + referred: Address, +) -> Result<(), SwapTradeError> { // Authentication referred.require_auth(); @@ -82,7 +86,7 @@ pub fn register_referral(env: &Env, referrer: Address, referred: Address) -> Res level: ReferralLevel::Direct, registration_timestamp: env.ledger().timestamp(), }; - + let info_key = DataKey::ReferralInfo(referred.clone()); env.storage().instance().set(&info_key, &referral_info); @@ -91,8 +95,8 @@ pub fn register_referral(env: &Env, referrer: Address, referred: Address) -> Res // Emit event env.events().publish( - (Symbol::new(env, "referral_registered"),), - (referrer, referred, ReferralLevel::Direct) + symbol_short!("referral_registered"), + (referrer, referred, ReferralLevel::Direct), ); Ok(()) @@ -100,16 +104,24 @@ pub fn register_referral(env: &Env, referrer: Address, referred: Address) -> Res fn is_circular_referral(env: &Env, referrer: &Address, referred: &Address) -> bool { // Check if referrer is already referred by the referred user (direct circular) - if let Some(existing_referrer) = env.storage().instance().get::<_, Address>(&DataKey::Referrer(referrer.clone())) { + if let Some(existing_referrer) = env + .storage() + .instance() + .get::<_, Address>(&DataKey::Referrer(referrer.clone())) + { if existing_referrer == *referred { return true; } } - + // Check for indirect circular references up to 2 levels let mut current_referrer = referrer.clone(); for _ in 0..2 { - if let Some(next_referrer) = env.storage().instance().get::<_, Address>(&DataKey::Referrer(current_referrer.clone())) { + if let Some(next_referrer) = env + .storage() + .instance() + .get::<_, Address>(&DataKey::Referrer(current_referrer.clone())) + { if next_referrer == *referred { return true; } @@ -118,18 +130,18 @@ fn is_circular_referral(env: &Env, referrer: &Address, referred: &Address) -> bo break; } } - + false } fn update_referrer_stats(env: &Env, referrer: &Address, level: ReferralLevel) { let mut stats = get_referral_stats(env, referrer.clone()); - + match level { ReferralLevel::Direct => stats.direct_referrals += 1, ReferralLevel::Indirect => stats.indirect_referrals += 1, } - + let stats_key = DataKey::ReferralStats(referrer.clone()); env.storage().instance().set(&stats_key, &stats); } @@ -170,33 +182,46 @@ pub fn calculate_and_distribute_commission(env: &Env, trader: Address, fee_amoun update_user_trading_volume(env, trader.clone(), fee_amount); // Get direct referrer - if let Some(direct_referrer) = env.storage().instance().get::<_, Address>(&DataKey::Referrer(trader.clone())) { + if let Some(direct_referrer) = env + .storage() + .instance() + .get::<_, Address>(&DataKey::Referrer(trader.clone())) + { // Calculate direct commission let direct_commission = (fee_amount * tier_config.direct_commission_bps as i128) / 10000; - + if direct_commission > 0 { add_commission_balance(env, direct_referrer.clone(), direct_commission); - + // Update stats let mut stats = get_referral_stats(env, direct_referrer.clone()); stats.total_commission_earned += direct_commission; stats.total_referee_volume += fee_amount; - env.storage().instance().set(&DataKey::ReferralStats(direct_referrer.clone()), &stats); + env.storage() + .instance() + .set(&DataKey::ReferralStats(direct_referrer.clone()), &stats); } // Get indirect referrer (referrer's referrer) - if let Some(indirect_referrer) = env.storage().instance().get::<_, Address>(&DataKey::Referrer(direct_referrer.clone())) { + if let Some(indirect_referrer) = env + .storage() + .instance() + .get::<_, Address>(&DataKey::Referrer(direct_referrer.clone())) + { // Calculate indirect commission - let indirect_commission = (fee_amount * tier_config.indirect_commission_bps as i128) / 10000; - + let indirect_commission = + (fee_amount * tier_config.indirect_commission_bps as i128) / 10000; + if indirect_commission > 0 { add_commission_balance(env, indirect_referrer.clone(), indirect_commission); - + // Update stats let mut stats = get_referral_stats(env, indirect_referrer.clone()); stats.total_commission_earned += indirect_commission; stats.total_referee_volume += fee_amount; - env.storage().instance().set(&DataKey::ReferralStats(indirect_referrer.clone()), &stats); + env.storage() + .instance() + .set(&DataKey::ReferralStats(indirect_referrer.clone()), &stats); } } } @@ -211,7 +236,10 @@ fn get_user_trading_volume(env: &Env, user: Address) -> i128 { fn update_user_trading_volume(env: &Env, user: Address, additional_volume: i128) { let current_volume = get_user_trading_volume(env, user.clone()); - env.storage().instance().set(&DataKey::TradingVolume(user), &(current_volume + additional_volume)); + env.storage().instance().set( + &DataKey::TradingVolume(user), + &(current_volume + additional_volume), + ); } fn add_commission_balance(env: &Env, user: Address, amount: i128) { @@ -220,8 +248,11 @@ fn add_commission_balance(env: &Env, user: Address, amount: i128) { .instance() .get(&DataKey::CommissionBalance(user.clone())) .unwrap_or(0); - - env.storage().instance().set(&DataKey::CommissionBalance(user), &(current_balance + amount)); + + env.storage().instance().set( + &DataKey::CommissionBalance(user), + &(current_balance + amount), + ); } pub fn withdraw_commission(env: &Env, user: Address) -> i128 { @@ -238,13 +269,13 @@ pub fn withdraw_commission(env: &Env, user: Address) -> i128 { } // Reset balance to zero before transfer (security best practice) - env.storage().instance().set(&DataKey::CommissionBalance(user.clone()), &0); + env.storage() + .instance() + .set(&DataKey::CommissionBalance(user.clone()), &0); // Emit event - env.events().publish( - (Symbol::new(env, "commission_withdrawn"),), - (user, balance) - ); + env.events() + .publish(symbol_short!("commission_withdrawn"), (user, balance)); balance } @@ -255,3 +286,8 @@ pub fn get_commission_balance(env: &Env, user: Address) -> i128 { .get(&DataKey::CommissionBalance(user)) .unwrap_or(0) } + +#[cfg(test)] +mod referral_integration_test; +#[cfg(test)] +mod referral_system_tests; diff --git a/swaptrade-contracts/counter/src/referral_system_tests.rs b/swaptrade-contracts/counter/src/referral_system_tests.rs index 97582eb..6a5aff9 100644 --- a/swaptrade-contracts/counter/src/referral_system_tests.rs +++ b/swaptrade-contracts/counter/src/referral_system_tests.rs @@ -25,7 +25,11 @@ use soroban_sdk::{Env, Address, Symbol, symbol_short}; assert!(register_referral(&env, referrer.clone(), referred.clone()).is_ok()); // Verify referrer is set - let stored_referrer = env.storage().instance().get::<_, Address>(&DataKey::Referrer(referred.clone())).unwrap(); + let stored_referrer = env + .storage() + .instance() + .get::<_, Address>(&DataKey::Referrer(referred.clone())) + .unwrap(); assert_eq!(stored_referrer, referrer); // Verify stats are updated @@ -38,7 +42,10 @@ use soroban_sdk::{Env, Address, Symbol, symbol_short}; fn test_self_referral_prevention() { let (env, user, _, _) = setup_test_env(); - assert_eq!(register_referral(&env, user.clone(), user.clone()), Err(SwapTradeError::SelfReferral)); + assert_eq!( + register_referral(&env, user.clone(), user.clone()), + Err(SwapTradeError::SelfReferral) + ); } #[test] @@ -49,7 +56,10 @@ use soroban_sdk::{Env, Address, Symbol, symbol_short}; assert!(register_referral(&env, referrer1.clone(), referred.clone()).is_ok()); // Second referral should fail - assert_eq!(register_referral(&env, referrer2.clone(), referred.clone()), Err(SwapTradeError::AlreadyReferred)); + assert_eq!( + register_referral(&env, referrer2.clone(), referred.clone()), + Err(SwapTradeError::AlreadyReferred) + ); } #[test] @@ -60,7 +70,10 @@ use soroban_sdk::{Env, Address, Symbol, symbol_short}; assert!(register_referral(&env, user_a.clone(), user_b.clone()).is_ok()); // Try circular referral: B refers A - assert_eq!(register_referral(&env, user_b.clone(), user_a.clone()), Err(SwapTradeError::CircularReferral)); + assert_eq!( + register_referral(&env, user_b.clone(), user_a.clone()), + Err(SwapTradeError::CircularReferral) + ); } #[test] @@ -74,7 +87,10 @@ use soroban_sdk::{Env, Address, Symbol, symbol_short}; assert!(register_referral(&env, user_b.clone(), user_c.clone()).is_ok()); // Try C refers A (should fail - indirect circular) - assert_eq!(register_referral(&env, user_c.clone(), user_a.clone()), Err(SwapTradeError::CircularReferral)); + assert_eq!( + register_referral(&env, user_c.clone(), user_a.clone()), + Err(SwapTradeError::CircularReferral) + ); } #[test] @@ -103,7 +119,9 @@ use soroban_sdk::{Env, Address, Symbol, symbol_short}; let (env, indirect_referrer, direct_referrer, trader) = setup_test_env(); // Set up two-level referral: indirect -> direct -> trader - assert!(register_referral(&env, indirect_referrer.clone(), direct_referrer.clone()).is_ok()); + assert!( + register_referral(&env, indirect_referrer.clone(), direct_referrer.clone()).is_ok() + ); assert!(register_referral(&env, direct_referrer.clone(), trader.clone()).is_ok()); // Simulate fee collection @@ -131,17 +149,17 @@ use soroban_sdk::{Env, Address, Symbol, symbol_short}; #[test] fn test_tier_upgrade() { let env = Env::default(); - + // Test tier 1 (default) let tier_1 = get_tier_for_volume(&env, 1000); assert_eq!(tier_1.direct_commission_bps, 50); assert_eq!(tier_1.indirect_commission_bps, 20); - + // Test tier 2 let tier_2 = get_tier_for_volume(&env, 15000); assert_eq!(tier_2.direct_commission_bps, 75); assert_eq!(tier_2.indirect_commission_bps, 30); - + // Test tier 3 let tier_3 = get_tier_for_volume(&env, 60000); assert_eq!(tier_3.direct_commission_bps, 100); @@ -222,7 +240,11 @@ use soroban_sdk::{Env, Address, Symbol, symbol_short}; assert!(register_referral(&env, referrer.clone(), referred.clone()).is_ok()); // Check referral info - let referral_info = env.storage().instance().get::<_, ReferralInfo>(&DataKey::ReferralInfo(referred.clone())).unwrap(); + let referral_info = env + .storage() + .instance() + .get::<_, ReferralInfo>(&DataKey::ReferralInfo(referred.clone())) + .unwrap(); assert_eq!(referral_info.referrer, referrer); assert_eq!(referral_info.level, ReferralLevel::Direct); assert!(referral_info.registration_timestamp > 0); diff --git a/swaptrade-contracts/counter/src/risk_management/alerts.rs b/swaptrade-contracts/counter/src/risk_management/alerts.rs index 166e441..9d5a07d 100644 --- a/swaptrade-contracts/counter/src/risk_management/alerts.rs +++ b/swaptrade-contracts/counter/src/risk_management/alerts.rs @@ -1,8 +1,10 @@ use soroban_sdk::{Env, Symbol, Vec}; pub fn send_alert(env: &Env, user: Symbol, message: Symbol) { - let key = (Symbol::new(env, "alerts"), user); - let mut alerts: Vec = env.storage().temporary().get(&key).unwrap_or_else(|| Vec::new(env)); - alerts.push_back(message); - env.storage().temporary().set(&key, &alerts); -} \ No newline at end of file + let mut alerts: Vec = env + .storage() + .get_unchecked(&format!("alerts_{}", user)) + .unwrap_or_default(); + alerts.push(message); + env.storage().set(&format!("alerts_{}", user), &alerts); +} diff --git a/swaptrade-contracts/counter/src/risk_management/circuit_breaker.rs b/swaptrade-contracts/counter/src/risk_management/circuit_breaker.rs index d6d1b5c..b53b465 100644 --- a/swaptrade-contracts/counter/src/risk_management/circuit_breaker.rs +++ b/swaptrade-contracts/counter/src/risk_management/circuit_breaker.rs @@ -1,6 +1,6 @@ -use soroban_sdk::{contracttype, Env, Map, Symbol, Vec, Address}; use crate::oracle::{get_stored_price, ContractError}; -use crate::risk_management::{RiskConfig, CircuitBreakerState}; +use crate::risk_management::{CircuitBreakerState, RiskConfig}; +use soroban_sdk::{contracttype, Address, Env, Map, Symbol, Vec}; /// Circuit breaker for extreme market moves pub struct CircuitBreaker; @@ -82,19 +82,24 @@ impl CircuitBreaker { let mut prices = Vec::new(env); // Try to get current price - if let Some(current_data) = get_stored_price(env, (asset_symbol.clone(), Symbol::short("USD"))) { + if let Some(current_data) = + get_stored_price(env, (asset_symbol.clone(), Symbol::short("USD"))) + { if current_data.timestamp >= start_time && current_data.timestamp <= end_time { prices.push_back((current_data.timestamp, current_data.price)); } } // Try inverse pair - if let Some(current_data) = get_stored_price(env, (Symbol::short("USD"), asset_symbol.clone())) { + if let Some(current_data) = + get_stored_price(env, (Symbol::short("USD"), asset_symbol.clone())) + { if current_data.timestamp >= start_time && current_data.timestamp <= end_time { // Invert price if current_data.price > 0 { - let inverted = (1_000_000_000_000_000_000u128 * 1_000_000_000_000_000_000u128) / current_data.price; - prices.push_back((current_data.timestamp, inverted)); + let inverted = (1_000_000_000_000_000_000u128 * 1_000_000_000_000_000_000u128) + / current_data.price; + prices.push((current_data.timestamp, inverted)); } } } @@ -150,4 +155,4 @@ impl CircuitBreaker { .get(&Symbol::short("risk_cfg")) .unwrap_or_default() } -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/risk_management/concentration_risk.rs b/swaptrade-contracts/counter/src/risk_management/concentration_risk.rs index 0b79208..fd667a6 100644 --- a/swaptrade-contracts/counter/src/risk_management/concentration_risk.rs +++ b/swaptrade-contracts/counter/src/risk_management/concentration_risk.rs @@ -1,24 +1,21 @@ -use soroban_sdk::{contracttype, Env, Map, Symbol, Vec, Address}; -use crate::portfolio::{Portfolio, Asset}; +use crate::portfolio::{Asset, Portfolio}; use crate::risk_management::{RiskConfig, RiskMetrics}; +use soroban_sdk::{contracttype, Address, Env, Map, Symbol, Vec}; /// Portfolio concentration risk monitoring pub struct ConcentrationRisk; impl ConcentrationRisk { /// Calculate portfolio concentration risk - pub fn calculate_concentration_risk( - env: &Env, - portfolio: &Portfolio, - user: &Address, - ) -> u32 { + pub fn calculate_concentration_risk(env: &Env, portfolio: &Portfolio, user: &Address) -> u32 { let total_value = Self::calculate_portfolio_value(env, portfolio, user); if total_value == 0 { return 0; } let xlm_value = portfolio.balance_of(env, Asset::XLM, user.clone()); - let usdc_value = portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); + let usdc_value = + portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); // Calculate concentration as percentage of largest position let max_position = xlm_value.max(usdc_value); @@ -26,13 +23,17 @@ impl ConcentrationRisk { // Convert to risk score (0-100) // Risk increases exponentially as concentration approaches 100% - if concentration_bps >= 8000 { // 80% + if concentration_bps >= 8000 { + // 80% 100 - } else if concentration_bps >= 6000 { // 60% + } else if concentration_bps >= 6000 { + // 60% 80 + ((concentration_bps - 6000) * 20) / 2000 - } else if concentration_bps >= 4000 { // 40% + } else if concentration_bps >= 4000 { + // 40% 60 + ((concentration_bps - 4000) * 20) / 2000 - } else if concentration_bps >= 2000 { // 20% + } else if concentration_bps >= 2000 { + // 20% 40 + ((concentration_bps - 2000) * 20) / 2000 } else { (concentration_bps * 40) / 2000 @@ -40,11 +41,7 @@ impl ConcentrationRisk { } /// Check if concentration exceeds warning threshold - pub fn check_concentration_warning( - env: &Env, - portfolio: &Portfolio, - user: &Address, - ) -> bool { + pub fn check_concentration_warning(env: &Env, portfolio: &Portfolio, user: &Address) -> bool { let config = Self::get_risk_config(env); let total_value = Self::calculate_portfolio_value(env, portfolio, user); if total_value == 0 { @@ -52,7 +49,8 @@ impl ConcentrationRisk { } let xlm_value = portfolio.balance_of(env, Asset::XLM, user.clone()); - let usdc_value = portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); + let usdc_value = + portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); let max_position = xlm_value.max(usdc_value); let concentration_bps = ((max_position * 10000) / total_value) as u32; @@ -61,11 +59,7 @@ impl ConcentrationRisk { } /// Check if concentration exceeds limit threshold (should block trades) - pub fn check_concentration_limit( - env: &Env, - portfolio: &Portfolio, - user: &Address, - ) -> bool { + pub fn check_concentration_limit(env: &Env, portfolio: &Portfolio, user: &Address) -> bool { let config = Self::get_risk_config(env); let total_value = Self::calculate_portfolio_value(env, portfolio, user); if total_value == 0 { @@ -73,7 +67,8 @@ impl ConcentrationRisk { } let xlm_value = portfolio.balance_of(env, Asset::XLM, user.clone()); - let usdc_value = portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); + let usdc_value = + portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); let max_position = xlm_value.max(usdc_value); let concentration_bps = ((max_position * 10000) / total_value) as u32; @@ -93,21 +88,19 @@ impl ConcentrationRisk { } let xlm_value = portfolio.balance_of(env, Asset::XLM, user.clone()); - let usdc_value = portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); + let usdc_value = + portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); let max_position = xlm_value.max(usdc_value); ((max_position * 10000) / total_value) as u32 } /// Calculate total portfolio value - fn calculate_portfolio_value( - env: &Env, - portfolio: &Portfolio, - user: &Address, - ) -> i128 { + fn calculate_portfolio_value(env: &Env, portfolio: &Portfolio, user: &Address) -> i128 { // Simple sum for now - in production should use oracle prices let xlm_balance = portfolio.balance_of(env, Asset::XLM, user.clone()); - let usdc_balance = portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); + let usdc_balance = + portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); xlm_balance + usdc_balance } @@ -119,4 +112,4 @@ impl ConcentrationRisk { .get(&Symbol::short("risk_cfg")) .unwrap_or_default() } -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/risk_management/mod.rs b/swaptrade-contracts/counter/src/risk_management/mod.rs index 018c9e2..822eeb9 100644 --- a/swaptrade-contracts/counter/src/risk_management/mod.rs +++ b/swaptrade-contracts/counter/src/risk_management/mod.rs @@ -1,17 +1,13 @@ -pub mod portfolio; -pub mod position; -pub mod volatility; pub mod alerts; pub mod circuit_breaker; -pub mod volume_circuit_breaker; -pub mod risk_metrics; -pub mod position_limits; pub mod concentration_risk; +pub mod portfolio; +pub mod position; +pub mod position_limits; +pub mod risk_metrics; +pub mod volatility; pub use circuit_breaker::*; -pub use volume_circuit_breaker::*; -pub use risk_metrics::*; -pub use position_limits::*; pub use concentration_risk::*; -pub use portfolio::*; -pub use position::*; +pub use position_limits::*; +pub use risk_metrics::*; diff --git a/swaptrade-contracts/counter/src/risk_management/portfolio.rs b/swaptrade-contracts/counter/src/risk_management/portfolio.rs index 996fb28..948da5c 100644 --- a/swaptrade-contracts/counter/src/risk_management/portfolio.rs +++ b/swaptrade-contracts/counter/src/risk_management/portfolio.rs @@ -1,34 +1,32 @@ -use soroban_sdk::{Env, Symbol, Map, Address}; use crate::portfolio::Portfolio; -use crate::risk_management::{RiskMetrics, ConcentrationRisk, CircuitBreaker, RiskConfig}; +use crate::risk_management::{CircuitBreaker, ConcentrationRisk, RiskConfig, RiskMetrics}; +use soroban_sdk::{Address, Env, Map, Symbol}; /// Portfolio risk assessment pub struct PortfolioRisk; impl PortfolioRisk { /// Calculate comprehensive risk metrics for a user - pub fn calculate_risk_metrics( - env: &Env, - portfolio: &Portfolio, - user: &Address, - ) -> RiskMetrics { + pub fn calculate_risk_metrics(env: &Env, portfolio: &Portfolio, user: &Address) -> RiskMetrics { let config = Self::get_risk_config(env); // Calculate individual risk components - let concentration_risk = ConcentrationRisk::calculate_concentration_risk(env, portfolio, user); + let concentration_risk = + ConcentrationRisk::calculate_concentration_risk(env, portfolio, user); let position_size_risk = Self::calculate_position_size_risk(env, portfolio, user); let volatility_risk = Self::calculate_volatility_risk(env); // Calculate overall risk score using weighted average - let overall_risk_score = ( - (concentration_risk as u32 * config.risk_weights.concentration_weight) + - (position_size_risk as u32 * config.risk_weights.position_size_weight) + - (volatility_risk as u32 * config.risk_weights.volatility_weight) - ) / 100; + let overall_risk_score = ((concentration_risk as u32 + * config.risk_weights.concentration_weight) + + (position_size_risk as u32 * config.risk_weights.position_size_weight) + + (volatility_risk as u32 * config.risk_weights.volatility_weight)) + / 100; // Calculate exposure and other metrics let total_exposure_usd = Self::calculate_total_exposure(env, portfolio, user); - let largest_position_pct = ConcentrationRisk::get_largest_position_percentage(env, portfolio, user); + let largest_position_pct = + ConcentrationRisk::get_largest_position_percentage(env, portfolio, user); let positions_over_limit = Self::count_positions_over_limit(env, portfolio, user); let circuit_breaker_active = CircuitBreaker::is_circuit_breaker_active(env); @@ -46,11 +44,7 @@ impl PortfolioRisk { } /// Calculate position size risk (0-100) - fn calculate_position_size_risk( - env: &Env, - portfolio: &Portfolio, - user: &Address, - ) -> u32 { + fn calculate_position_size_risk(env: &Env, portfolio: &Portfolio, user: &Address) -> u32 { let config = Self::get_risk_config(env); let user_tier = portfolio.get_user_tier(env, user.clone()); @@ -59,8 +53,10 @@ impl PortfolioRisk { let mut position_count = 0u32; for (_, size) in positions.iter() { - let size_abs = if size < 0 { -size } else { size }; - let max_allowed = crate::risk_management::PositionLimits::get_tier_position_limit(&config, &user_tier); + let size_abs = if size < 0 { -size } else { *size }; + let max_allowed = crate::risk_management::PositionLimits::get_tier_position_limit( + &config, &user_tier, + ); if max_allowed > 0 { let utilization = (size_abs * 100) / max_allowed; @@ -102,30 +98,28 @@ impl PortfolioRisk { } /// Calculate total exposure in USD - fn calculate_total_exposure( - env: &Env, - portfolio: &Portfolio, - user: &Address, - ) -> i128 { + fn calculate_total_exposure(env: &Env, portfolio: &Portfolio, user: &Address) -> i128 { // Simple calculation - in production should use oracle prices let xlm_balance = portfolio.balance_of(env, crate::portfolio::Asset::XLM, user.clone()); - let usdc_balance = portfolio.balance_of(env, crate::portfolio::Asset::Custom(Symbol::short("USDCSIM")), user.clone()); + let usdc_balance = portfolio.balance_of( + env, + crate::portfolio::Asset::Custom(Symbol::short("USDCSIM")), + user.clone(), + ); // Assume 1 XLM = 1 USD for simulation xlm_balance + usdc_balance } /// Count positions that exceed limits - fn count_positions_over_limit( - env: &Env, - portfolio: &Portfolio, - user: &Address, - ) -> u32 { + fn count_positions_over_limit(env: &Env, portfolio: &Portfolio, user: &Address) -> u32 { let positions = Self::get_user_positions(env, portfolio, user); let mut over_limit = 0u32; for (asset, _) in positions.iter() { - if let Err(_) = crate::risk_management::PositionLimits::check_position_limits(env, portfolio, user, &asset, 0) { + if let Err(_) = crate::risk_management::PositionLimits::check_position_limits( + env, portfolio, user, &asset, 0, + ) { over_limit += 1; } } @@ -142,13 +136,20 @@ impl PortfolioRisk { let mut positions = Map::new(env); let xlm_balance = portfolio.balance_of(env, crate::portfolio::Asset::XLM, user.clone()); - let usdc_balance = portfolio.balance_of(env, crate::portfolio::Asset::Custom(Symbol::short("USDCSIM")), user.clone()); + let usdc_balance = portfolio.balance_of( + env, + crate::portfolio::Asset::Custom(Symbol::short("USDCSIM")), + user.clone(), + ); if xlm_balance != 0 { positions.set(crate::portfolio::Asset::XLM, xlm_balance); } if usdc_balance != 0 { - positions.set(crate::portfolio::Asset::Custom(Symbol::short("USDCSIM")), usdc_balance); + positions.set( + crate::portfolio::Asset::Custom(Symbol::short("USDCSIM")), + usdc_balance, + ); } positions @@ -161,4 +162,4 @@ impl PortfolioRisk { .get(&Symbol::short("risk_cfg")) .unwrap_or_default() } -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/risk_management/position.rs b/swaptrade-contracts/counter/src/risk_management/position.rs index 4268a0a..751a858 100644 --- a/swaptrade-contracts/counter/src/risk_management/position.rs +++ b/swaptrade-contracts/counter/src/risk_management/position.rs @@ -1,6 +1,6 @@ -use soroban_sdk::{Env, Symbol, Address, Map}; -use crate::portfolio::{Portfolio, Asset}; -use crate::risk_management::{PositionLimits, PositionLimitError}; +use crate::portfolio::{Asset, Portfolio}; +use crate::risk_management::{PositionLimitError, PositionLimits}; +use soroban_sdk::{Address, Env, Map, Symbol}; /// Position management and limits pub struct PositionManager; @@ -37,7 +37,8 @@ impl PositionManager { // For now, just XLM and USDC positions let xlm_balance = portfolio.balance_of(env, Asset::XLM, user.clone()); - let usdc_balance = portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); + let usdc_balance = + portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); if xlm_balance != 0 { positions.set(Asset::XLM, xlm_balance); @@ -50,11 +51,7 @@ impl PositionManager { } /// Check if user has exceeded position limits - pub fn has_exceeded_limits( - env: &Env, - portfolio: &Portfolio, - user: &Address, - ) -> bool { + pub fn has_exceeded_limits(env: &Env, portfolio: &Portfolio, user: &Address) -> bool { let positions = Self::get_user_positions(env, portfolio, user); for (asset, size) in positions.iter() { @@ -65,4 +62,4 @@ impl PositionManager { false } -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/risk_management/position_limits.rs b/swaptrade-contracts/counter/src/risk_management/position_limits.rs index e35ee55..4e519c0 100644 --- a/swaptrade-contracts/counter/src/risk_management/position_limits.rs +++ b/swaptrade-contracts/counter/src/risk_management/position_limits.rs @@ -1,7 +1,7 @@ -use soroban_sdk::{contracttype, Env, Map, Symbol, Vec, Address}; -use crate::portfolio::{Portfolio, Asset}; -use crate::tiers::{UserTier, calculate_user_tier}; +use crate::portfolio::{Asset, Portfolio}; use crate::risk_management::RiskConfig; +use crate::tiers::{calculate_user_tier, UserTier}; +use soroban_sdk::{contracttype, Address, Env, Map, Symbol, Vec}; /// Position limit enforcement pub struct PositionLimits; @@ -49,20 +49,20 @@ impl PositionLimits { /// Get position limit based on user tier pub fn get_tier_position_limit(config: &RiskConfig, tier: &UserTier) -> i128 { match tier { - UserTier::Novice => config.max_position_per_asset / 10, // 10% of base - UserTier::Trader => config.max_position_per_asset / 4, // 25% of base - UserTier::Expert => config.max_position_per_asset / 2, // 50% of base - UserTier::Whale => config.max_position_per_asset, // 100% of base + UserTier::Novice => config.max_position_per_asset / 10, // 10% of base + UserTier::Trader => config.max_position_per_asset / 4, // 25% of base + UserTier::Expert => config.max_position_per_asset / 2, // 50% of base + UserTier::Whale => config.max_position_per_asset, // 100% of base } } /// Get portfolio limit based on user tier pub fn get_tier_portfolio_limit(config: &RiskConfig, tier: &UserTier) -> i128 { match tier { - UserTier::Novice => config.max_position_per_user / 10, // 10% of base - UserTier::Trader => config.max_position_per_user / 4, // 25% of base - UserTier::Expert => config.max_position_per_user / 2, // 50% of base - UserTier::Whale => config.max_position_per_user, // 100% of base + UserTier::Novice => config.max_position_per_user / 10, // 10% of base + UserTier::Trader => config.max_position_per_user / 4, // 25% of base + UserTier::Expert => config.max_position_per_user / 2, // 50% of base + UserTier::Whale => config.max_position_per_user, // 100% of base } } @@ -75,7 +75,8 @@ impl PositionLimits { // For simplicity, sum all balances (assuming same decimals) // In production, this should convert to USD value using oracles let xlm_balance = portfolio.balance_of(env, Asset::XLM, user.clone()); - let usdc_balance = portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); + let usdc_balance = + portfolio.balance_of(env, Asset::Custom(Symbol::short("USDCSIM")), user.clone()); xlm_balance + usdc_balance } @@ -94,7 +95,9 @@ impl PositionLimits { if !config.risk_weights.validate() { panic!("Risk weights must sum to 100"); } - env.storage().instance().set(&Symbol::short("risk_cfg"), config); + env.storage() + .instance() + .set(&Symbol::short("risk_cfg"), config); } } @@ -110,4 +113,4 @@ pub enum PositionLimitError { requested: i128, limit: i128, }, -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/risk_management/risk_metrics.rs b/swaptrade-contracts/counter/src/risk_management/risk_metrics.rs index d4e27bf..0abaeac 100644 --- a/swaptrade-contracts/counter/src/risk_management/risk_metrics.rs +++ b/swaptrade-contracts/counter/src/risk_management/risk_metrics.rs @@ -1,5 +1,5 @@ -use soroban_sdk::{contracttype, Env, Map, Symbol, Vec, Address}; use crate::portfolio::Asset; +use soroban_sdk::{contracttype, Address, Env, Map, Symbol, Vec}; /// Risk metrics returned by get_risk_metrics() #[contracttype] @@ -69,11 +69,11 @@ impl Default for RiskConfig { fn default() -> Self { Self { max_position_per_user: 1000000000000, // 1M tokens (with 6 decimals) - max_position_per_asset: 500000000000, // 500K tokens per asset - concentration_warn_threshold: 3000, // 30% - concentration_limit_threshold: 5000, // 50% - circuit_breaker_threshold: 1500, // 15% - circuit_breaker_window: 3600, // 1 hour + max_position_per_asset: 500000000000, // 500K tokens per asset + concentration_warning_threshold: 3000, // 30% + concentration_limit_threshold: 5000, // 50% + circuit_breaker_threshold: 1500, // 15% + circuit_breaker_window: 3600, // 1 hour risk_weights: RiskWeights { concentration_weight: 40, position_size_weight: 35, @@ -110,4 +110,4 @@ impl Default for CircuitBreakerState { recovery_price: None, } } -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/risk_management/volatility.rs b/swaptrade-contracts/counter/src/risk_management/volatility.rs index f43c9e0..4458808 100644 --- a/swaptrade-contracts/counter/src/risk_management/volatility.rs +++ b/swaptrade-contracts/counter/src/risk_management/volatility.rs @@ -2,7 +2,13 @@ use soroban_sdk::{Env, Symbol}; pub fn check_circuit_breaker(env: &Env, asset: Symbol) -> bool { // Example: trigger breaker if volatility exceeds threshold - let key = (Symbol::new(env, "vol"), asset); - let volatility: i32 = env.storage().temporary().get(&key).unwrap_or(0); - volatility > 50 -} \ No newline at end of file + let volatility: i32 = env + .storage() + .get_unchecked(&format!("vol_{}", asset)) + .unwrap_or(0); + if volatility > 50 { + true // Circuit breaker triggered + } else { + false + } +} diff --git a/swaptrade-contracts/counter/src/risk_management_tests.rs b/swaptrade-contracts/counter/src/risk_management_tests.rs index 1cb51cf..3422060 100644 --- a/swaptrade-contracts/counter/src/risk_management_tests.rs +++ b/swaptrade-contracts/counter/src/risk_management_tests.rs @@ -1,15 +1,13 @@ #[cfg(test)] mod risk_management_tests { use super::*; - use crate::CounterContract; - use soroban_sdk::{testutils::Address as _, testutils::Ledger as _, Address, Env, Symbol, symbol_short}; - use crate::portfolio::{Portfolio, Asset}; - use crate::tiers::UserTier; + use crate::portfolio::{Asset, Portfolio}; use crate::risk_management::{ - RiskConfig, RiskMetrics, CircuitBreakerState, - PositionLimits, ConcentrationRisk, CircuitBreaker, PortfolioRisk, - PositionLimitError, + CircuitBreaker, CircuitBreakerState, ConcentrationRisk, PortfolioRisk, PositionLimitError, + PositionLimits, RiskConfig, RiskMetrics, }; + use crate::tiers::UserTier; + use soroban_sdk::{symbol_short, testutils::Address as _, Address, Env, Symbol}; // ===== POSITION LIMITS TESTS ===== @@ -79,7 +77,12 @@ mod risk_management_tests { // Balanced portfolio: 50% XLM, 50% USDC portfolio.credit(&env, Asset::XLM, user.clone(), 500); - portfolio.credit(&env, Asset::Custom(symbol_short!("USDCSIM")), user.clone(), 500); + portfolio.credit( + &env, + Asset::Custom(symbol_short!("USDCSIM")), + user.clone(), + 500, + ); let risk = ConcentrationRisk::calculate_concentration_risk(&env, &portfolio, &user); assert!(risk < 30); // Should be low risk @@ -93,7 +96,12 @@ mod risk_management_tests { // Highly concentrated: 95% XLM, 5% USDC portfolio.credit(&env, Asset::XLM, user.clone(), 950); - portfolio.credit(&env, Asset::Custom(symbol_short!("USDCSIM")), user.clone(), 50); + portfolio.credit( + &env, + Asset::Custom(symbol_short!("USDCSIM")), + user.clone(), + 50, + ); let risk = ConcentrationRisk::calculate_concentration_risk(&env, &portfolio, &user); assert!(risk > 70); // Should be high risk @@ -107,7 +115,12 @@ mod risk_management_tests { // 35% concentration (above 30% threshold) portfolio.credit(&env, Asset::XLM, user.clone(), 700); - portfolio.credit(&env, Asset::Custom(symbol_short!("USDCSIM")), user.clone(), 300); + portfolio.credit( + &env, + Asset::Custom(symbol_short!("USDCSIM")), + user.clone(), + 300, + ); let warning = ConcentrationRisk::check_concentration_warning(&env, &portfolio, &user); assert!(warning); @@ -161,7 +174,12 @@ mod risk_management_tests { // Set up a test portfolio portfolio.credit(&env, Asset::XLM, user.clone(), 600); - portfolio.credit(&env, Asset::Custom(symbol_short!("USDCSIM")), user.clone(), 400); + portfolio.credit( + &env, + Asset::Custom(symbol_short!("USDCSIM")), + user.clone(), + 400, + ); let metrics = PortfolioRisk::calculate_risk_metrics(&env, &portfolio, &user); @@ -202,12 +220,8 @@ mod risk_management_tests { let to = symbol_short!("USDCSIM"); let amount = 100; // Small amount should be OK - let would_exceed = CounterContract::check_risk_limits( - env.clone(), - user.clone(), - to, - amount, - ); + let would_exceed = + CounterContract::check_risk_limits(env.clone(), user.clone(), to, amount); assert!(!would_exceed); } @@ -220,7 +234,12 @@ mod risk_management_tests { // Create highly concentrated portfolio portfolio.credit(&env, Asset::XLM, user.clone(), 900); - portfolio.credit(&env, Asset::Custom(symbol_short!("USDCSIM")), user.clone(), 100); + portfolio.credit( + &env, + Asset::Custom(symbol_short!("USDCSIM")), + user.clone(), + 100, + ); // Save portfolio to storage for contract functions env.storage().instance().set(&(), &portfolio); @@ -304,4 +323,4 @@ mod risk_management_tests { let retrieved_config = PositionLimits::get_risk_config(&env); assert_eq!(retrieved_config.max_position_per_user, 2000000000000); } -} \ No newline at end of file +} diff --git a/swaptrade-contracts/counter/src/staking_bonus.rs b/swaptrade-contracts/counter/src/staking_bonus.rs index 8aad2b7..215cb38 100644 --- a/swaptrade-contracts/counter/src/staking_bonus.rs +++ b/swaptrade-contracts/counter/src/staking_bonus.rs @@ -1,3 +1,4 @@ +use crate::errors::SwapTradeError; /// Staking Bonus System /// /// Provides long-term staking bonuses with: @@ -12,7 +13,6 @@ /// - 90 days: 20% bonus /// - 365 days: 50% bonus use soroban_sdk::{contracttype, symbol_short, Address, Env, Vec}; -use crate::errors::SwapTradeError; // ──────────────────────────────────────────────────────────────────────────── // Constants @@ -254,7 +254,11 @@ impl StakingBonusManager { /// /// # Returns /// Result with (principal_returned, penalty) or error - pub fn unstake_early(env: &Env, user: Address, stake_id: u32) -> Result<(i128, i128), SwapTradeError> { + pub fn unstake_early( + env: &Env, + user: Address, + stake_id: u32, + ) -> Result<(i128, i128), SwapTradeError> { let mut stakes: Vec = env .storage() .persistent() diff --git a/swaptrade-contracts/counter/src/state_snapshot.rs b/swaptrade-contracts/counter/src/state_snapshot.rs index 6a56e8e..d580c38 100644 --- a/swaptrade-contracts/counter/src/state_snapshot.rs +++ b/swaptrade-contracts/counter/src/state_snapshot.rs @@ -10,7 +10,7 @@ //! - Use local variables for state reads //! - Validate before commit -use soroban_sdk::{contracttype, Env, Address, Symbol}; +use soroban_sdk::{contracttype, Address, Env, Symbol}; /// Snapshot of critical state for validation #[contracttype] @@ -47,7 +47,10 @@ impl StateSnapshotManager { /// Get next snapshot ID fn get_next_snapshot_id(env: &Env) -> u64 { - let key = (soroban_sdk::symbol_short!("snap"), soroban_sdk::symbol_short!("id")); + let key = ( + soroban_sdk::symbol_short!("snap"), + soroban_sdk::symbol_short!("id"), + ); let current_id: u64 = env.storage().temporary().get(&key).unwrap_or(0); let next_id = current_id + 1; env.storage().temporary().set(&key, &next_id); @@ -60,7 +63,7 @@ pub struct AtomicOperation; impl AtomicOperation { /// Execute an atomic operation with state validation - /// + /// /// This function ensures that: /// 1. All state is read before any mutations /// 2. State is validated before committing changes @@ -123,9 +126,9 @@ impl StateConsistencyChecker { new_state: &T, allowed_transitions: &[(T, T)], ) -> bool { - allowed_transitions.iter().any(|(from, to)| { - from == old_state && to == new_state - }) + allowed_transitions + .iter() + .any(|(from, to)| from == old_state && to == new_state) } /// Validate that all required preconditions are met before state mutation @@ -137,10 +140,7 @@ impl StateConsistencyChecker { } /// Execute state mutation with pre and post validation - pub fn execute_with_validation( - operation: F, - validator: V, - ) -> Result + pub fn execute_with_validation(operation: F, validator: V) -> Result where F: FnOnce() -> R, V: FnOnce(&R) -> bool, diff --git a/swaptrade-contracts/counter/src/state_snapshot_tests.rs b/swaptrade-contracts/counter/src/state_snapshot_tests.rs index 6806271..d1fd876 100644 --- a/swaptrade-contracts/counter/src/state_snapshot_tests.rs +++ b/swaptrade-contracts/counter/src/state_snapshot_tests.rs @@ -115,8 +115,7 @@ mod tests { #[test] fn test_execute_with_validation_failure() { - let result = - StateConsistencyChecker::execute_with_validation(|| 42, |value| *value == 100); + let result = StateConsistencyChecker::execute_with_validation(|| 42, |value| *value == 100); assert!(result.is_err()); } diff --git a/swaptrade-contracts/counter/src/storage.rs b/swaptrade-contracts/counter/src/storage.rs index 00c41e9..87db3e6 100644 --- a/swaptrade-contracts/counter/src/storage.rs +++ b/swaptrade-contracts/counter/src/storage.rs @@ -1,8 +1,9 @@ -use soroban_sdk::{contracttype, symbol_short, Symbol, Address}; +use soroban_sdk::{contracttype, symbol_short, Address, Symbol}; pub const ADMIN_KEY: Symbol = symbol_short!("admin"); pub const PAUSED_KEY: Symbol = symbol_short!("paused"); pub const POOL_REGISTRY_KEY: Symbol = symbol_short!("pools"); +pub const DEFAULT_TREASURY_KEY: Symbol = symbol_short!("treasury"); #[contracttype] #[derive(Clone)] @@ -11,7 +12,8 @@ pub enum DataKey { Admin, Paused, PoolRegistry, - + DefaultTreasury, + // Referral system keys Referrer(Address), ReferralInfo(Address),