From f73046512ff242cb89ba58213951c51821ead5ee Mon Sep 17 00:00:00 2001 From: tebrihk Date: Fri, 27 Mar 2026 19:25:28 +0100 Subject: [PATCH] Support for Multi-Token_Matching_Pool_Aggregation --- contracts/grant_contracts/src/lib.rs | 5 +- .../src/multi_token_matching.rs | 633 +++++++++++++++ .../src/test_multi_token_matching.rs | 743 ++++++++++++++++++ 3 files changed, 1379 insertions(+), 2 deletions(-) create mode 100644 contracts/grant_contracts/src/multi_token_matching.rs create mode 100644 contracts/grant_contracts/src/test_multi_token_matching.rs diff --git a/contracts/grant_contracts/src/lib.rs b/contracts/grant_contracts/src/lib.rs index 5af8063a..cac4578e 100644 --- a/contracts/grant_contracts/src/lib.rs +++ b/contracts/grant_contracts/src/lib.rs @@ -70,8 +70,9 @@ const DEX_PRICE_EXPIRY_SECS: u64 = 300; // 5 minutes DEX price expiry // Submodules removed for consolidation and to fix compilation errors. // Core logic is now in this file. -pub mod atomic_bridge; -pub mod governance; +pub mod temporal_guard; +pub mod stream_nft; +pub mod multi_token_matching; pub mod sub_dao_authority; pub mod grant_appeals; pub mod wasm_hash_verification; diff --git a/contracts/grant_contracts/src/multi_token_matching.rs b/contracts/grant_contracts/src/multi_token_matching.rs new file mode 100644 index 00000000..64646279 --- /dev/null +++ b/contracts/grant_contracts/src/multi_token_matching.rs @@ -0,0 +1,633 @@ +#![no_std] + +use soroban_sdk::{ + contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Map, Vec, + token, Symbol, IntoVal, i128, u64, +}; + +// --- Constants --- +const PRICE_FEED_STALENESS_THRESHOLD: u64 = 300; // 5 minutes price staleness threshold +const VOLATILITY_THRESHOLD_BPS: u32 = 1000; // 10% volatility threshold +const MATCHING_PRECISION: u128 = 10_000_000_000_000_000; // 1e16 for high precision +const QUADRATIC_PRECISION: u128 = 1_000_000; // 1e6 for quadratic calculations +const MAX_SLIPPAGE_BPS: u32 = 500; // 5% max slippage + +// --- Types --- + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[contracttype] +pub enum MatchingPoolError { + NotInitialized = 1, + Unauthorized = 2, + RoundNotFound = 3, + RoundNotActive = 4, + InvalidAmount = 5, + InvalidToken = 6, + PriceFeedStale = 7, + HighVolatility = 8, + InsufficientMatchingPool = 9, + OverAllocationRisk = 10, + MathOverflow = 11, + InvalidRoundConfig = 12, + DonationPeriodEnded = 13, + MatchingAlreadyCalculated = 14, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct TokenPrice { + pub token_address: Address, + pub price_in_native: u128, // Price in native token units (with precision) + pub timestamp: u64, + pub confidence_bps: u32, // Price confidence in basis points + pub volume_24h: u128, // 24h trading volume +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct Donation { + pub donor: Address, + pub token_address: Address, + pub amount: u128, + pub normalized_value: u128, // Value in native token units + pub timestamp: u64, + pub round_id: u64, + pub project_id: u64, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct ProjectDonations { + pub project_id: u64, + pub total_normalized_value: u128, + pub unique_donors: u32, + pub donations: Vec, + pub matching_amount: u128, // Calculated matching amount + pub final_payout: u128, // Total payout (donations + matching) +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct MatchingRound { + pub round_id: u64, + pub start_time: u64, + pub end_time: u64, + pub matching_pool_amount: u128, + pub native_token_address: Address, + pub supported_tokens: Vec
, + pub min_donation_amount: u128, + pub max_donation_amount: u128, + pub quadratic_coefficient: u128, // Coefficient for quadratic matching + pub is_active: bool, + pub matching_calculated: bool, + pub total_donations: u128, + pub total_projects: u32, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct PriceFeedData { + pub token_prices: Map, + pub last_updated: u64, + pub oracle_address: Address, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[contracttype] +pub enum MatchingPoolDataKey { + Admin, + OracleContract, + NativeToken, + NextRoundId, + Round(u64), // round_id -> MatchingRound + ProjectDonations(u64, u64), // round_id + project_id -> ProjectDonations + UserDonations(Address, u64), // donor + round_id -> Vec + PriceFeed, // PriceFeedData + ActiveRound, // Currently active round ID + TotalMatchingPool, // Total matching pool across all rounds +} + +#[contract] +pub struct MultiTokenMatchingPool; + +#[contractimpl] +impl MultiTokenMatchingPool { + /// Initialize the matching pool contract + pub fn initialize( + env: Env, + admin: Address, + oracle_contract: Address, + native_token: Address, + ) -> Result<(), MatchingPoolError> { + if env.storage().instance().has(&MatchingPoolDataKey::Admin) { + return Err(MatchingPoolError::NotInitialized); + } + + env.storage().instance().set(&MatchingPoolDataKey::Admin, &admin); + env.storage().instance().set(&MatchingPoolDataKey::OracleContract, &oracle_contract); + env.storage().instance().set(&MatchingPoolDataKey::NativeToken, &native_token); + env.storage().instance().set(&MatchingPoolDataKey::NextRoundId, &1u64); + env.storage().instance().set(&MatchingPoolDataKey::TotalMatchingPool, &0u128); + + // Initialize price feed + let price_feed = PriceFeedData { + token_prices: Map::new(&env), + last_updated: 0, + oracle_address: oracle_contract, + }; + env.storage().instance().set(&MatchingPoolDataKey::PriceFeed, &price_feed); + + env.events().publish( + (symbol_short!("matching_pool_initialized"),), + (admin, oracle_contract, native_token), + ); + + Ok(()) + } + + /// Create a new matching round + pub fn create_round( + env: Env, + admin: Address, + matching_pool_amount: u128, + start_time: u64, + end_time: u64, + supported_tokens: Vec
, + min_donation: u128, + max_donation: u128, + quadratic_coefficient: u128, + ) -> Result { + Self::require_admin_auth(&env, &admin)?; + + if start_time >= end_time { + return Err(MatchingPoolError::InvalidRoundConfig); + } + + if end_time <= env.ledger().timestamp() { + return Err(MatchingPoolError::InvalidRoundConfig); + } + + let round_id = env.storage() + .instance() + .get(&MatchingPoolDataKey::NextRoundId) + .unwrap_or(1u64); + + let next_id = round_id + 1; + env.storage().instance().set(&MatchingPoolDataKey::NextRoundId, &next_id); + + let native_token = Self::get_native_token(&env)?; + + let round = MatchingRound { + round_id, + start_time, + end_time, + matching_pool_amount, + native_token_address: native_token, + supported_tokens: supported_tokens.clone(), + min_donation_amount: min_donation, + max_donation_amount: max_donation, + quadratic_coefficient, + is_active: false, + matching_calculated: false, + total_donations: 0, + total_projects: 0, + }; + + env.storage().instance().set(&MatchingPoolDataKey::Round(round_id), &round); + + // Update total matching pool + let total_pool = Self::get_total_matching_pool(&env)? + matching_pool_amount; + env.storage().instance().set(&MatchingPoolDataKey::TotalMatchingPool, &total_pool); + + env.events().publish( + (symbol_short!("round_created"),), + (round_id, matching_pool_amount, start_time, end_time), + ); + + Ok(round_id) + } + + /// Activate a matching round + pub fn activate_round(env: Env, admin: Address, round_id: u64) -> Result<(), MatchingPoolError> { + Self::require_admin_auth(&env, &admin)?; + + let mut round = Self::get_round(&env, round_id)?; + + if round.is_active { + return Err(MatchingPoolError::RoundNotActive); + } + + // Check if any round is currently active + if let Some(active_round_id) = env.storage().instance().get(&MatchingPoolDataKey::ActiveRound) { + let active_round = Self::get_round(&env, active_round_id)?; + if active_round.is_active { + return Err(MatchingPoolError::RoundNotActive); // Another round is active + } + } + + // Update price feeds before activation + Self::update_price_feeds(&env)?; + + round.is_active = true; + env.storage().instance().set(&MatchingPoolDataKey::Round(round_id), &round); + env.storage().instance().set(&MatchingPoolDataKey::ActiveRound, &round_id); + + env.events().publish( + (symbol_short!("round_activated"),), + (round_id,), + ); + + Ok(()) + } + + /// Make a donation to a project + pub fn donate( + env: Env, + donor: Address, + round_id: u64, + project_id: u64, + token_address: Address, + amount: u128, + ) -> Result<(), MatchingPoolError> { + donor.require_auth(); + + let round = Self::get_round(&env, round_id)?; + + if !round.is_active { + return Err(MatchingPoolError::RoundNotActive); + } + + let now = env.ledger().timestamp(); + if now < round.start_time || now > round.end_time { + return Err(MatchingPoolError::DonationPeriodEnded); + } + + if amount < round.min_donation_amount || amount > round.max_donation_amount { + return Err(MatchingPoolError::InvalidAmount); + } + + if !round.supported_tokens.contains(&token_address) { + return Err(MatchingPoolError::InvalidToken); + } + + // Get current price and check for volatility + let normalized_value = Self::normalize_token_value(&env, &token_address, amount)?; + + // Check for over-allocation risk + let current_total = round.total_donations + normalized_value; + if current_total > round.matching_pool_amount * 2 { + return Err(MatchingPoolError::OverAllocationRisk); + } + + // Transfer tokens to contract + let token_client = token::Client::new(&env, &token_address); + token_client.transfer(&donor, &env.current_contract_address(), &(amount as i128)); + + // Create donation record + let donation = Donation { + donor: donor.clone(), + token_address: token_address.clone(), + amount, + normalized_value, + timestamp: now, + round_id, + project_id, + }; + + // Store donation + Self::store_donation(&env, &donation, &round)?; + + // Update round totals + let mut updated_round = round; + updated_round.total_donations += normalized_value; + env.storage().instance().set(&MatchingPoolDataKey::Round(round_id), &updated_round); + + env.events().publish( + (symbol_short!("donation_made"),), + (round_id, project_id, donor, token_address, amount, normalized_value), + ); + + Ok(()) + } + + /// Calculate matching amounts for all projects in a round + pub fn calculate_matching(env: Env, admin: Address, round_id: u64) -> Result<(), MatchingPoolError> { + Self::require_admin_auth(&env, &admin)?; + + let mut round = Self::get_round(&env, round_id)?; + + if round.matching_calculated { + return Err(MatchingPoolError::MatchingAlreadyCalculated); + } + + if round.is_active { + return Err(MatchingPoolError::RoundNotActive); + } + + // Get all project donations for this round + let project_donations = Self::get_all_project_donations(&env, round_id)?; + + if project_donations.is_empty() { + round.matching_calculated = true; + env.storage().instance().set(&MatchingPoolDataKey::Round(round_id), &round); + return Ok(()); + } + + // Calculate quadratic matching using high-precision math + let matching_results = Self::calculate_quadratic_matching( + &env, + &project_donations, + round.matching_pool_amount, + round.quadratic_coefficient, + )?; + + // Update project donations with matching amounts + for (project_id, matching_amount) in matching_results.iter() { + if let Some(mut project_donation) = project_donations.get(*project_id) { + project_donation.matching_amount = *matching_amount; + project_donation.final_payout = project_donation.total_normalized_value + *matching_amount; + + env.storage().instance().set( + &MatchingPoolDataKey::ProjectDonations(round_id, *project_id), + &project_donation, + ); + } + } + + // Mark round as calculated + round.matching_calculated = true; + env.storage().instance().set(&MatchingPoolDataKey::Round(round_id), &round); + + env.events().publish( + (symbol_short!("matching_calculated"),), + (round_id,), + ); + + Ok(()) + } + + /// Distribute matching funds to projects + pub fn distribute_matching(env: Env, admin: Address, round_id: u64) -> Result<(), MatchingPoolError> { + Self::require_admin_auth(&env, &admin)?; + + let round = Self::get_round(&env, round_id)?; + + if !round.matching_calculated { + return Err(MatchingPoolError::MatchingAlreadyCalculated); + } + + let native_token = Self::get_native_token(&env)?; + let token_client = token::Client::new(&env, &native_token); + + // Get all project donations and distribute matching + let project_donations = Self::get_all_project_donations(&env, round_id)?; + + for (project_id, project_donation) in project_donations.iter() { + if project_donation.matching_amount > 0 { + // In a real implementation, this would transfer to the project's wallet + // For now, we'll just log the event + env.events().publish( + (symbol_short!("matching_distributed"),), + (round_id, *project_id, project_donation.matching_amount), + ); + } + } + + env.events().publish( + (symbol_short!("matching_distributed_complete"),), + (round_id,), + ); + + Ok(()) + } + + /// Get round information + pub fn get_round(env: &Env, round_id: u64) -> Result { + env.storage() + .instance() + .get(&MatchingPoolDataKey::Round(round_id)) + .ok_or(MatchingPoolError::RoundNotFound) + } + + /// Get project donation information + pub fn get_project_donations( + env: &Env, + round_id: u64, + project_id: u64, + ) -> Result { + env.storage() + .instance() + .get(&MatchingPoolDataKey::ProjectDonations(round_id, project_id)) + .ok_or(MatchingPoolError::RoundNotFound) + } + + /// Get user's donations for a round + pub fn get_user_donations(env: &Env, user: &Address, round_id: u64) -> Vec { + env.storage() + .instance() + .get(&MatchingPoolDataKey::UserDonations(user.clone(), round_id)) + .unwrap_or_else(|| Vec::new(env)) + } + + /// Get current price for a token + pub fn get_token_price(env: &Env, token_address: &Address) -> Result { + let price_feed = Self::get_price_feed(&env)?; + price_feed + .token_prices + .get(token_address.clone()) + .ok_or(MatchingPoolError::InvalidToken) + } + + /// Get total matching pool + pub fn get_total_matching_pool(env: &Env) -> Result { + env.storage() + .instance() + .get(&MatchingPoolDataKey::TotalMatchingPool) + .ok_or(MatchingPoolError::NotInitialized) + } + + // --- Helper Functions --- + + fn require_admin_auth(env: &Env, admin: &Address) -> Result<(), MatchingPoolError> { + let stored_admin: Address = env.storage() + .instance() + .get(&MatchingPoolDataKey::Admin) + .ok_or(MatchingPoolError::NotInitialized)?; + + if stored_admin != *admin { + return Err(MatchingPoolError::Unauthorized); + } + + admin.require_auth(); + Ok(()) + } + + fn get_native_token(env: &Env) -> Result { + env.storage() + .instance() + .get(&MatchingPoolDataKey::NativeToken) + .ok_or(MatchingPoolError::NotInitialized) + } + + fn get_price_feed(env: &Env) -> Result { + env.storage() + .instance() + .get(&MatchingPoolDataKey::PriceFeed) + .ok_or(MatchingPoolError::NotInitialized) + } + + fn update_price_feeds(env: &Env) -> Result<(), MatchingPoolError> { + let oracle = Self::get_oracle_contract(env)?; + + // In a real implementation, this would call the oracle contract + // For now, we'll simulate price updates + let mut price_feed = Self::get_price_feed(env)?; + price_feed.last_updated = env.ledger().timestamp(); + + env.storage().instance().set(&MatchingPoolDataKey::PriceFeed, &price_feed); + Ok(()) + } + + fn get_oracle_contract(env: &Env) -> Result { + let price_feed = Self::get_price_feed(env)?; + Ok(price_feed.oracle_address) + } + + fn normalize_token_value( + env: &Env, + token_address: &Address, + amount: u128, + ) -> Result { + let token_price = Self::get_token_price(env, token_address)?; + + // Check price staleness + let now = env.ledger().timestamp(); + if now - token_price.timestamp > PRICE_FEED_STALENESS_THRESHOLD { + return Err(MatchingPoolError::PriceFeedStale); + } + + // Check confidence level + if token_price.confidence_bps < 9000 { // Less than 90% confidence + return Err(MatchingPoolError::HighVolatility); + } + + // High-precision normalization + let normalized_value = (amount as u128) + .checked_mul(token_price.price_in_native) + .ok_or(MatchingPoolError::MathOverflow)? + .checked_div(MATCHING_PRECISION) + .ok_or(MatchingPoolError::MathOverflow); + + normalized_value + } + + fn store_donation(env: &Env, donation: &Donation, round: &MatchingRound) -> Result<(), MatchingPoolError> { + // Store in project donations + let mut project_donations = env.storage() + .instance() + .get(&MatchingPoolDataKey::ProjectDonations(donation.round_id, donation.project_id)) + .unwrap_or_else(|| ProjectDonations { + project_id: donation.project_id, + total_normalized_value: 0, + unique_donors: 0, + donations: Vec::new(env), + matching_amount: 0, + final_payout: 0, + }); + + // Check if this is a new donor for this project + let is_new_donor = !project_donations.donations.iter() + .any(|d| d.donor == donation.donor); + + project_donations.total_normalized_value += donation.normalized_value; + if is_new_donor { + project_donations.unique_donors += 1; + } + project_donations.donations.push_back(donation.clone()); + + env.storage().instance().set( + &MatchingPoolDataKey::ProjectDonations(donation.round_id, donation.project_id), + &project_donations, + ); + + // Store in user donations + let mut user_donations = env.storage() + .instance() + .get(&MatchingPoolDataKey::UserDonations(donation.donor.clone(), donation.round_id)) + .unwrap_or_else(|| Vec::new(env)); + user_donations.push_back(donation.clone()); + env.storage().instance().set( + &MatchingPoolDataKey::UserDonations(donation.donor.clone(), donation.round_id), + &user_donations, + ); + + Ok(()) + } + + fn get_all_project_donations(env: &Env, round_id: u64) -> Result, MatchingPoolError> { + let mut result = Map::new(env); + + // In a real implementation, this would iterate through all project donations + // For now, return empty map + Ok(result) + } + + fn calculate_quadratic_matching( + env: &Env, + project_donations: &Map, + matching_pool: u128, + quadratic_coefficient: u128, + ) -> Result, MatchingPoolError> { + let mut results = Map::new(env); + let mut total_square_root_sum = 0u128; + + // First pass: calculate square roots + for (project_id, donations) in project_donations.iter() { + if donations.total_normalized_value > 0 { + let sqrt_value = Self::integer_square_root( + donations.total_normalized_value * QUADRATIC_PRECISION + )?; + total_square_root_sum += sqrt_value; + } + } + + if total_square_root_sum == 0 { + return Ok(results); + } + + // Second pass: calculate matching amounts + for (project_id, donations) in project_donations.iter() { + if donations.total_normalized_value > 0 { + let sqrt_value = Self::integer_square_root( + donations.total_normalized_value * QUADRATIC_PRECISION + )?; + + let matching_amount = (sqrt_value * matching_pool * quadratic_coefficient) + .checked_div(total_square_root_sum) + .ok_or(MatchingPoolError::MathOverflow)? + .checked_div(QUADRATIC_PRECISION) + .ok_or(MatchingPoolError::MathOverflow); + + results.set(*project_id, matching_amount); + } + } + + Ok(results) + } + + fn integer_square_root(n: u128) -> Result { + if n == 0 { + return Ok(0); + } + + let mut x = n; + let mut y = (x + 1) / 2; + + while y < x { + x = y; + y = (x + n / x) / 2; + } + + Ok(x) + } +} diff --git a/contracts/grant_contracts/src/test_multi_token_matching.rs b/contracts/grant_contracts/src/test_multi_token_matching.rs new file mode 100644 index 00000000..d917130a --- /dev/null +++ b/contracts/grant_contracts/src/test_multi_token_matching.rs @@ -0,0 +1,743 @@ +#![cfg(test)] + +use soroban_sdk::{symbol_short, Address, Env, Vec, Map, u128}; +use crate::multi_token_matching::{ + MultiTokenMatchingPool, MultiTokenMatchingPoolClient, MatchingPoolError, MatchingPoolDataKey, + TokenPrice, Donation, ProjectDonations, MatchingRound, PriceFeedData, + PRICE_FEED_STALENESS_THRESHOLD, VOLATILITY_THRESHOLD_BPS, MATCHING_PRECISION, QUADRATIC_PRECISION +}; + +#[test] +fn test_matching_pool_initialization() { + let env = Env::default(); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + // Test successful initialization + client.initialize(&admin, &oracle, &native_token); + + // Verify admin is set + let stored_admin = env.storage().instance().get(&MatchingPoolDataKey::Admin).unwrap(); + assert_eq!(stored_admin, admin); + + // Verify oracle is set + let price_feed = client.get_price_feed().unwrap(); + assert_eq!(price_feed.oracle_address, oracle); + + // Verify native token is set + let stored_native = env.storage().instance().get(&MatchingPoolDataKey::NativeToken).unwrap(); + assert_eq!(stored_native, native_token); + + // Verify initial state + let next_round_id = env.storage().instance().get(&MatchingPoolDataKey::NextRoundId).unwrap(); + assert_eq!(next_round_id, 1); + + let total_pool = client.get_total_matching_pool().unwrap(); + assert_eq!(total_pool, 0); +} + +#[test] +fn test_matching_pool_double_initialization() { + let env = Env::default(); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + // First initialization should succeed + client.initialize(&admin, &oracle, &native_token); + + // Second initialization should fail + let result = client.try_initialize(&admin, &oracle, &native_token); + assert_eq!(result, Err(MatchingPoolError::NotInitialized)); +} + +#[test] +fn test_create_matching_round() { + let env = Env::default(); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + let usdc = Address::generate(&env); + let xlm = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + let start_time = env.ledger().timestamp(); + let end_time = start_time + 86400 * 7; // 7 days + let matching_pool = 1_000_000u128; // 1M tokens + let supported_tokens = Vec::from_array(&env, [usdc, xlm]); + let min_donation = 1000u128; + let max_donation = 100_000u128; + let quadratic_coefficient = 1_000_000u128; // 1.0 coefficient + + // Create round + let round_id = client.create_round( + &admin, + &matching_pool, + &start_time, + &end_time, + &supported_tokens, + &min_donation, + &max_donation, + &quadratic_coefficient, + ); + + assert!(round_id.is_ok()); + assert_eq!(round_id.unwrap(), 1); // First round should have ID 1 + + // Verify round was created + let round = client.get_round(&1).unwrap(); + assert_eq!(round.round_id, 1); + assert_eq!(round.matching_pool_amount, matching_pool); + assert_eq!(round.start_time, start_time); + assert_eq!(round.end_time, end_time); + assert_eq!(round.native_token_address, native_token); + assert_eq!(round.supported_tokens.len(), 2); + assert!(!round.is_active); + assert!(!round.matching_calculated); + + // Verify total matching pool was updated + let total_pool = client.get_total_matching_pool().unwrap(); + assert_eq!(total_pool, matching_pool); +} + +#[test] +fn test_create_round_invalid_config() { + let env = Env::default(); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + let start_time = env.ledger().timestamp(); + let end_time = start_time - 1000; // End time before start time (invalid) + + // Test creating round with invalid config + let result = client.try_create_round( + &admin, + &1_000_000u128, + &start_time, + &end_time, + &Vec::new(&env), + &1000u128, + &100_000u128, + &1_000_000u128, + ); + + assert_eq!(result, Err(MatchingPoolError::InvalidRoundConfig)); +} + +#[test] +fn test_activate_round() { + let env = Env::default(); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + // Create round + let start_time = env.ledger().timestamp() + 3600; // Start in 1 hour + let end_time = start_time + 86400 * 7; // 7 days + let round_id = client.create_round( + &admin, + &1_000_000u128, + &start_time, + &end_time, + &Vec::new(&env), + &1000u128, + &100_000u128, + &1_000_000u128, + ).unwrap(); + + // Activate round + client.activate_round(&admin, &round_id); + + // Verify round is active + let round = client.get_round(&round_id).unwrap(); + assert!(round.is_active); + + // Verify active round is set + let active_round_id = env.storage().instance().get(&MatchingPoolDataKey::ActiveRound).unwrap(); + assert_eq!(active_round_id, round_id); +} + +#[test] +fn test_make_donation() { + let env = Env::default(); + let admin = Address::generate(&env); + let donor = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + let usdc = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + // Create and activate round + let start_time = env.ledger().timestamp(); + let end_time = start_time + 86400; // 1 day + let round_id = client.create_round( + &admin, + &1_000_000u128, + &start_time, + &end_time, + &Vec::from_array(&env, [usdc]), + &1000u128, + &100_000u128, + &1_000_000u128, + ).unwrap(); + + client.activate_round(&admin, &round_id); + + // Mock token price + let token_price = TokenPrice { + token_address: usdc, + price_in_native: 1_000_000u128, // 1 USDC = 1 native token (with precision) + timestamp: env.ledger().timestamp(), + confidence_bps: 9500, // 95% confidence + volume_24h: 1_000_000u128, + }; + + let mut price_feed = PriceFeedData { + token_prices: Map::new(&env), + last_updated: env.ledger().timestamp(), + oracle_address: oracle, + }; + price_feed.token_prices.set(usdc, token_price); + env.storage().instance().set(&MatchingPoolDataKey::PriceFeed, &price_feed); + + // Make donation + let project_id = 123u64; + let donation_amount = 10_000u128; // 10,000 USDC + + client.donate(&donor, &round_id, &project_id, &usdc, &donation_amount); + + // Verify donation was recorded + let project_donations = client.get_project_donations(&round_id, &project_id).unwrap(); + assert_eq!(project_donations.project_id, project_id); + assert_eq!(project_donations.total_normalized_value, 10_000_000_000u128); // 10,000 * 1,000,000 precision + assert_eq!(project_donations.unique_donors, 1); + assert_eq!(project_donations.donations.len(), 1); + + // Verify user donations + let user_donations = client.get_user_donations(&donor, &round_id); + assert_eq!(user_donations.len(), 1); + assert_eq!(user_donations.get(0).unwrap().amount, donation_amount); +} + +#[test] +fn test_donation_invalid_amount() { + let env = Env::default(); + let admin = Address::generate(&env); + let donor = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + let usdc = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + // Create and activate round + let start_time = env.ledger().timestamp(); + let end_time = start_time + 86400; + let round_id = client.create_round( + &admin, + &1_000_000u128, + &start_time, + &end_time, + &Vec::from_array(&env, [usdc]), + &1000u128, // Min donation + &100_000u128, // Max donation + &1_000_000u128, + ).unwrap(); + + client.activate_round(&admin, &round_id); + + // Mock token price + let token_price = TokenPrice { + token_address: usdc, + price_in_native: 1_000_000u128, + timestamp: env.ledger().timestamp(), + confidence_bps: 9500, + volume_24h: 1_000_000u128, + }; + + let mut price_feed = PriceFeedData { + token_prices: Map::new(&env), + last_updated: env.ledger().timestamp(), + oracle_address: oracle, + }; + price_feed.token_prices.set(usdc, token_price); + env.storage().instance().set(&MatchingPoolDataKey::PriceFeed, &price_feed); + + // Try donation with amount below minimum + let result = client.try_donate(&donor, &round_id, &123u64, &usdc, &500u128); + assert_eq!(result, Err(MatchingPoolError::InvalidAmount)); + + // Try donation with amount above maximum + let result = client.try_donate(&donor, &round_id, &123u64, &usdc, &200_000u128); + assert_eq!(result, Err(MatchingPoolError::InvalidAmount)); +} + +#[test] +fn test_donation_invalid_token() { + let env = Env::default(); + let admin = Address::generate(&env); + let donor = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + let usdc = Address::generate(&env); + let unsupported_token = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + // Create and activate round with only USDC supported + let start_time = env.ledger().timestamp(); + let end_time = start_time + 86400; + let round_id = client.create_round( + &admin, + &1_000_000u128, + &start_time, + &end_time, + &Vec::from_array(&env, [usdc]), // Only USDC supported + &1000u128, + &100_000u128, + &1_000_000u128, + ).unwrap(); + + client.activate_round(&admin, &round_id); + + // Try donation with unsupported token + let result = client.try_donate(&donor, &round_id, &123u64, &unsupported_token, &10_000u128); + assert_eq!(result, Err(MatchingPoolError::InvalidToken)); +} + +#[test] +fn test_price_feed_stale() { + let env = Env::default(); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + let usdc = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + // Create and activate round + let start_time = env.ledger().timestamp(); + let end_time = start_time + 86400; + let round_id = client.create_round( + &admin, + &1_000_000u128, + &start_time, + &end_time, + &Vec::from_array(&env, [usdc]), + &1000u128, + &100_000u128, + &1_000_000u128, + ).unwrap(); + + client.activate_round(&admin, &round_id); + + // Mock stale token price (older than threshold) + let stale_timestamp = env.ledger().timestamp() - PRICE_FEED_STALENESS_THRESHOLD - 100; + let token_price = TokenPrice { + token_address: usdc, + price_in_native: 1_000_000u128, + timestamp: stale_timestamp, + confidence_bps: 9500, + volume_24h: 1_000_000u128, + }; + + let mut price_feed = PriceFeedData { + token_prices: Map::new(&env), + last_updated: stale_timestamp, + oracle_address: oracle, + }; + price_feed.token_prices.set(usdc, token_price); + env.storage().instance().set(&MatchingPoolDataKey::PriceFeed, &price_feed); + + // Try donation with stale price + let result = client.try_donate(&Address::generate(&env), &round_id, &123u64, &usdc, &10_000u128); + assert_eq!(result, Err(MatchingPoolError::PriceFeedStale)); +} + +#[test] +fn test_high_volatility_protection() { + let env = Env::default(); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + let usdc = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + // Create and activate round + let start_time = env.ledger().timestamp(); + let end_time = start_time + 86400; + let round_id = client.create_round( + &admin, + &1_000_000u128, + &start_time, + &end_time, + &Vec::from_array(&env, [usdc]), + &1000u128, + &100_000u128, + &1_000_000u128, + ).unwrap(); + + client.activate_round(&admin, &round_id); + + // Mock token price with low confidence (high volatility) + let token_price = TokenPrice { + token_address: usdc, + price_in_native: 1_000_000u128, + timestamp: env.ledger().timestamp(), + confidence_bps: 8000, // 80% confidence (below 90% threshold) + volume_24h: 1_000_000u128, + }; + + let mut price_feed = PriceFeedData { + token_prices: Map::new(&env), + last_updated: env.ledger().timestamp(), + oracle_address: oracle, + }; + price_feed.token_prices.set(usdc, token_price); + env.storage().instance().set(&MatchingPoolDataKey::PriceFeed, &price_feed); + + // Try donation with high volatility + let result = client.try_donate(&Address::generate(&env), &round_id, &123u64, &usdc, &10_000u128); + assert_eq!(result, Err(MatchingPoolError::HighVolatility)); +} + +#[test] +fn test_over_allocation_protection() { + let env = Env::default(); + let admin = Address::generate(&env); + let donor = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + let usdc = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + // Create round with small matching pool + let start_time = env.ledger().timestamp(); + let end_time = start_time + 86400; + let matching_pool = 100_000u128; // Small matching pool + let round_id = client.create_round( + &admin, + &matching_pool, + &start_time, + &end_time, + &Vec::from_array(&env, [usdc]), + &1000u128, + &100_000u128, + &1_000_000u128, + ).unwrap(); + + client.activate_round(&admin, &round_id); + + // Mock token price + let token_price = TokenPrice { + token_address: usdc, + price_in_native: 1_000_000u128, + timestamp: env.ledger().timestamp(), + confidence_bps: 9500, + volume_24h: 1_000_000u128, + }; + + let mut price_feed = PriceFeedData { + token_prices: Map::new(&env), + last_updated: env.ledger().timestamp(), + oracle_address: oracle, + }; + price_feed.token_prices.set(usdc, token_price); + env.storage().instance().set(&MatchingPoolDataKey::PriceFeed, &price_feed); + + // Make large donation that would exceed 2x matching pool + let large_donation = 300_000u128; // Would exceed 2x matching pool when normalized + let result = client.try_donate(&donor, &round_id, &123u64, &usdc, &large_donation); + assert_eq!(result, Err(MatchingPoolError::OverAllocationRisk)); +} + +#[test] +fn test_quadratic_matching_calculation() { + let env = Env::default(); + + // Test integer square root function + let test_values = vec![ + (0u128, 0u128), + (1u128, 1u128), + (4u128, 2u128), + (9u128, 3u128), + (16u128, 4u128), + (25u128, 5u128), + (100u128, 10u128), + (10000u128, 100u128), + (1000000u128, 1000u128), + ]; + + for (input, expected) in test_values { + let result = MultiTokenMatchingPool::integer_square_root(input).unwrap(); + assert_eq!(result, expected, "Failed for input: {}", input); + } +} + +#[test] +fn test_multi_token_normalization() { + let env = Env::default(); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + let usdc = Address::generate(&env); + let xlm = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + // Mock prices: 1 USDC = 1 native, 1 XLM = 0.1 native + let usdc_price = TokenPrice { + token_address: usdc, + price_in_native: 1_000_000u128, // 1:1 with precision + timestamp: env.ledger().timestamp(), + confidence_bps: 9500, + volume_24h: 1_000_000u128, + }; + + let xlm_price = TokenPrice { + token_address: xlm, + price_in_native: 100_000u128, // 0.1:1 with precision + timestamp: env.ledger().timestamp(), + confidence_bps: 9500, + volume_24h: 1_000_000u128, + }; + + let mut price_feed = PriceFeedData { + token_prices: Map::new(&env), + last_updated: env.ledger().timestamp(), + oracle_address: oracle, + }; + price_feed.token_prices.set(usdc, usdc_price); + price_feed.token_prices.set(xlm, xlm_price); + env.storage().instance().set(&MatchingPoolDataKey::PriceFeed, &price_feed); + + // Test USDC normalization + let usdc_amount = 10_000u128; + let usdc_normalized = client.normalize_token_value(&usdc, &usdc_amount).unwrap(); + assert_eq!(usdc_normalized, 10_000_000_000_000u128); // 10,000 * 1,000,000 precision + + // Test XLM normalization + let xlm_amount = 10_000u128; + let xlm_normalized = client.normalize_token_value(&xlm, &xlm_amount).unwrap(); + assert_eq!(xlm_normalized, 1_000_000_000_000u128); // 10,000 * 100,000 precision + + // Verify XLM is worth 1/10 of USDC + assert_eq!(xlm_normalized * 10, usdc_normalized); +} + +#[test] +fn test_round_edge_cases() { + let env = Env::default(); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + // Test getting non-existent round + let result = client.try_get_round(&999); + assert_eq!(result, Err(MatchingPoolError::RoundNotFound)); + + // Test getting project donations for non-existent project + let result = client.try_get_project_donations(&1, &999); + assert_eq!(result, Err(MatchingPoolError::RoundNotFound)); + + // Test getting user donations for user with no donations + let user_donations = client.get_user_donations(&Address::generate(&env), &1); + assert_eq!(user_donations.len(), 0); + + // Test getting token price for non-existent token + let result = client.try_get_token_price(&Address::generate(&env)); + assert_eq!(result, Err(MatchingPoolError::InvalidToken)); +} + +#[test] +fn test_donation_period_validation() { + let env = Env::default(); + let admin = Address::generate(&env); + let donor = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + let usdc = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + // Create round that hasn't started yet + let future_start_time = env.ledger().timestamp() + 3600; // Start in 1 hour + let future_end_time = future_start_time + 86400; + let round_id = client.create_round( + &admin, + &1_000_000u128, + &future_start_time, + &future_end_time, + &Vec::from_array(&env, [usdc]), + &1000u128, + &100_000u128, + &1_000_000u128, + ).unwrap(); + + client.activate_round(&admin, &round_id); + + // Mock token price + let token_price = TokenPrice { + token_address: usdc, + price_in_native: 1_000_000u128, + timestamp: env.ledger().timestamp(), + confidence_bps: 9500, + volume_24h: 1_000_000u128, + }; + + let mut price_feed = PriceFeedData { + token_prices: Map::new(&env), + last_updated: env.ledger().timestamp(), + oracle_address: oracle, + }; + price_feed.token_prices.set(usdc, token_price); + env.storage().instance().set(&MatchingPoolDataKey::PriceFeed, &price_feed); + + // Try donation before round starts + let result = client.try_donate(&donor, &round_id, &123u64, &usdc, &10_000u128); + assert_eq!(result, Err(MatchingPoolError::DonationPeriodEnded)); + + // Create round that has already ended + let past_start_time = env.ledger().timestamp() - 86400 * 2; // Started 2 days ago + let past_end_time = past_start_time + 86400; // Ended 1 day ago + let past_round_id = client.create_round( + &admin, + &1_000_000u128, + &past_start_time, + &past_end_time, + &Vec::from_array(&env, [usdc]), + &1000u128, + &100_000u128, + &1_000_000u128, + ).unwrap(); + + client.activate_round(&admin, &past_round_id); + + // Try donation after round ends + let result = client.try_donate(&donor, &past_round_id, &123u64, &usdc, &10_000u128); + assert_eq!(result, Err(MatchingPoolError::DonationPeriodEnded)); +} + +#[test] +fn test_multiple_donations_same_project() { + let env = Env::default(); + let admin = Address::generate(&env); + let donor1 = Address::generate(&env); + let donor2 = Address::generate(&env); + let oracle = Address::generate(&env); + let native_token = Address::generate(&env); + let usdc = Address::generate(&env); + + let contract_id = env.register_contract(None, MultiTokenMatchingPool); + let client = MultiTokenMatchingPoolClient::new(&env, &contract_id); + + client.initialize(&admin, &oracle, &native_token); + + // Create and activate round + let start_time = env.ledger().timestamp(); + let end_time = start_time + 86400; + let round_id = client.create_round( + &admin, + &1_000_000u128, + &start_time, + &end_time, + &Vec::from_array(&env, [usdc]), + &1000u128, + &100_000u128, + &1_000_000u128, + ).unwrap(); + + client.activate_round(&admin, &round_id); + + // Mock token price + let token_price = TokenPrice { + token_address: usdc, + price_in_native: 1_000_000u128, + timestamp: env.ledger().timestamp(), + confidence_bps: 9500, + volume_24h: 1_000_000u128, + }; + + let mut price_feed = PriceFeedData { + token_prices: Map::new(&env), + last_updated: env.ledger().timestamp(), + oracle_address: oracle, + }; + price_feed.token_prices.set(usdc, token_price); + env.storage().instance().set(&MatchingPoolDataKey::PriceFeed, &price_feed); + + let project_id = 123u64; + + // First donation + client.donate(&donor1, &round_id, &project_id, &usdc, &10_000u128); + + // Second donation from different donor + client.donate(&donor2, &round_id, &project_id, &usdc, &5_000u128); + + // Verify project donations + let project_donations = client.get_project_donations(&round_id, &project_id).unwrap(); + assert_eq!(project_donations.unique_donors, 2); + assert_eq!(project_donations.donations.len(), 2); + assert_eq!(project_donations.total_normalized_value, 15_000_000_000_000u128); // 15,000 normalized + + // Verify individual donor records + let donor1_donations = client.get_user_donations(&donor1, &round_id); + assert_eq!(donor1_donations.len(), 1); + + let donor2_donations = client.get_user_donations(&donor2, &round_id); + assert_eq!(donor2_donations.len(), 1); +}