diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index 1ca0286..7ec7cfa 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -1,10 +1,16 @@ #![no_std] +mod safe_math; +#[cfg(test)] +mod safe_math_examples; + use soroban_sdk::{ contract, contracterror, contractevent, contractimpl, contracttype, token, Address, Env, IntoVal, String, Symbol, Vec, }; +pub use safe_math::{RoundingMode, SafeMath}; + const DAY_IN_LEDGERS: u32 = 17280; const BUMP_THRESHOLD: u32 = 14 * DAY_IN_LEDGERS; const BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; diff --git a/contract/contracts/predifi-contract/src/safe_math.rs b/contract/contracts/predifi-contract/src/safe_math.rs new file mode 100644 index 0000000..ae5b4dd --- /dev/null +++ b/contract/contracts/predifi-contract/src/safe_math.rs @@ -0,0 +1,580 @@ +#![allow(dead_code)] + +//! # Safe Math Module for Proportion Calculations +//! +//! This module provides safe arithmetic operations for proportion and percentage +//! calculations, critical for payout logic where rounding errors or division by +//! zero could lead to locked funds or unfair distributions. +//! +//! ## Features +//! +//! - Fixed-point arithmetic with configurable precision +//! - Protection against overflow, underflow, and division by zero +//! - Configurable rounding strategies (protocol-favoring, neutral, user-favoring) +//! - Proportion calculations that maintain fairness in payouts +//! +//! ## Usage Example +//! +//! ```rust,ignore +//! use safe_math::{SafeMath, RoundingMode}; +//! +//! // Calculate 30% of 1000 with protocol-favoring rounding +//! let result = SafeMath::percentage(1000, 3000, RoundingMode::ProtocolFavor)?; +//! +//! // Calculate proportional payout +//! let payout = SafeMath::proportion(user_stake, total_stake, pool_balance, RoundingMode::Neutral)?; +//! ``` + +use predifi_errors::PrediFiError; + +#[cfg(test)] +extern crate std; + +#[cfg(test)] +use std::vec::Vec; + +/// Fixed-point precision multiplier (10,000 = 0.01% precision) +/// This allows for basis point calculations (1 bps = 0.01%) +const PRECISION: i128 = 10_000; + +/// Maximum basis points (100% = 10,000 bps) +const MAX_BPS: i128 = 10_000; + +/// Rounding mode for calculations +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum RoundingMode { + /// Round down - favors the protocol by keeping dust in the pool + ProtocolFavor, + /// Round to nearest - neutral rounding + Neutral, + /// Round up - favors the user (use with caution) + UserFavor, +} + +/// Safe math operations for proportion and percentage calculations +pub struct SafeMath; + +impl SafeMath { + /// Calculate a percentage of an amount using basis points. + /// + /// # Arguments + /// * `amount` - The base amount + /// * `bps` - Basis points (e.g., 100 = 1%, 10000 = 100%) + /// * `rounding` - Rounding mode to use + /// + /// # Returns + /// The calculated percentage or an error + /// + /// # Example + /// ```rust,ignore + /// // Calculate 2.5% of 1000 = 25 + /// let result = SafeMath::percentage(1000, 250, RoundingMode::Neutral)?; + /// ``` + pub fn percentage( + amount: i128, + bps: i128, + rounding: RoundingMode, + ) -> Result { + // Validate inputs + if amount < 0 { + return Err(PrediFiError::ArithmeticError); + } + if !(0..=MAX_BPS).contains(&bps) { + return Err(PrediFiError::InvalidFeeBps); + } + if amount == 0 || bps == 0 { + return Ok(0); + } + + // Calculate: (amount * bps) / MAX_BPS + let numerator = amount + .checked_mul(bps) + .ok_or(PrediFiError::ArithmeticError)?; + + Self::divide_with_rounding(numerator, MAX_BPS, rounding) + } + + /// Calculate a proportion: (numerator / denominator) * amount + /// + /// This is the core function for payout calculations. + /// + /// # Arguments + /// * `numerator` - The user's stake or share + /// * `denominator` - The total stake or pool + /// * `amount` - The amount to distribute proportionally + /// * `rounding` - Rounding mode to use + /// + /// # Returns + /// The proportional amount or an error + /// + /// # Example + /// ```rust,ignore + /// // User staked 300 out of 1000 total, pool has 5000 to distribute + /// // Result: (300 / 1000) * 5000 = 1500 + /// let payout = SafeMath::proportion(300, 1000, 5000, RoundingMode::Neutral)?; + /// ``` + pub fn proportion( + numerator: i128, + denominator: i128, + amount: i128, + rounding: RoundingMode, + ) -> Result { + // Validate inputs + if numerator < 0 || denominator <= 0 || amount < 0 { + return Err(PrediFiError::ArithmeticError); + } + if numerator == 0 || amount == 0 { + return Ok(0); + } + if numerator > denominator { + return Err(PrediFiError::ArithmeticError); + } + + // Calculate: (numerator * amount) / denominator + let product = numerator + .checked_mul(amount) + .ok_or(PrediFiError::ArithmeticError)?; + + Self::divide_with_rounding(product, denominator, rounding) + } + + /// Safely divide two numbers with configurable rounding + /// + /// # Arguments + /// * `numerator` - The dividend + /// * `denominator` - The divisor + /// * `rounding` - Rounding mode to use + /// + /// # Returns + /// The quotient or an error + fn divide_with_rounding( + numerator: i128, + denominator: i128, + rounding: RoundingMode, + ) -> Result { + if denominator == 0 { + return Err(PrediFiError::ArithmeticError); + } + + let quotient = numerator + .checked_div(denominator) + .ok_or(PrediFiError::ArithmeticError)?; + let remainder = numerator + .checked_rem(denominator) + .ok_or(PrediFiError::ArithmeticError)?; + + match rounding { + RoundingMode::ProtocolFavor => { + // Always round down (floor) + Ok(quotient) + } + RoundingMode::Neutral => { + // Round to nearest (half up) + let half = denominator + .checked_div(2) + .ok_or(PrediFiError::ArithmeticError)?; + if remainder >= half { + quotient.checked_add(1).ok_or(PrediFiError::ArithmeticError) + } else { + Ok(quotient) + } + } + RoundingMode::UserFavor => { + // Round up (ceiling) if there's any remainder + if remainder > 0 { + quotient.checked_add(1).ok_or(PrediFiError::ArithmeticError) + } else { + Ok(quotient) + } + } + } + } + + /// Calculate multiple proportions ensuring the sum doesn't exceed the total + /// + /// This is useful for distributing payouts to multiple winners where rounding + /// errors could cause the sum to exceed the available pool balance. + /// + /// Note: This function is primarily for testing and validation. In production, + /// calculate payouts individually and track the distributed amount. + /// + /// # Arguments + /// * `stakes` - Array of individual stakes + /// * `total_stake` - Sum of all stakes + /// * `pool_balance` - Total amount to distribute + /// * `rounding` - Rounding mode to use + /// + /// # Returns + /// Vector of proportional amounts or an error + #[cfg(test)] + pub fn multi_proportion( + stakes: &[i128], + total_stake: i128, + pool_balance: i128, + rounding: RoundingMode, + ) -> Result, PrediFiError> { + if stakes.is_empty() { + return Ok(Vec::new()); + } + if total_stake <= 0 || pool_balance < 0 { + return Err(PrediFiError::ArithmeticError); + } + + let mut results = Vec::with_capacity(stakes.len()); + let mut distributed = 0i128; + + // Calculate proportions for all but the last + for (i, &stake) in stakes.iter().enumerate() { + if stake < 0 { + return Err(PrediFiError::ArithmeticError); + } + + if i == stakes.len() - 1 { + // Last entry gets the remainder to avoid rounding issues + let remaining = pool_balance + .checked_sub(distributed) + .ok_or(PrediFiError::ArithmeticError)?; + results.push(remaining); + } else { + let amount = Self::proportion(stake, total_stake, pool_balance, rounding)?; + distributed = distributed + .checked_add(amount) + .ok_or(PrediFiError::ArithmeticError)?; + results.push(amount); + } + } + + // Verify we didn't over-distribute + let total_distributed: i128 = results.iter().sum(); + if total_distributed > pool_balance { + return Err(PrediFiError::RewardError); + } + + Ok(results) + } + + /// Safely add two amounts with overflow check + pub fn safe_add(a: i128, b: i128) -> Result { + a.checked_add(b).ok_or(PrediFiError::ArithmeticError) + } + + /// Safely subtract two amounts with underflow check + pub fn safe_sub(a: i128, b: i128) -> Result { + a.checked_sub(b).ok_or(PrediFiError::ArithmeticError) + } + + /// Safely multiply two amounts with overflow check + pub fn safe_mul(a: i128, b: i128) -> Result { + a.checked_mul(b).ok_or(PrediFiError::ArithmeticError) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::vec; + + #[test] + fn test_percentage_basic() { + // 10% of 1000 = 100 + assert_eq!( + SafeMath::percentage(1000, 1000, RoundingMode::Neutral).unwrap(), + 100 + ); + + // 2.5% of 1000 = 25 + assert_eq!( + SafeMath::percentage(1000, 250, RoundingMode::Neutral).unwrap(), + 25 + ); + + // 100% of 1000 = 1000 + assert_eq!( + SafeMath::percentage(1000, 10000, RoundingMode::Neutral).unwrap(), + 1000 + ); + + // 0% of 1000 = 0 + assert_eq!( + SafeMath::percentage(1000, 0, RoundingMode::Neutral).unwrap(), + 0 + ); + } + + #[test] + fn test_percentage_rounding() { + // 3.33% of 100 = 3.33 + // Protocol favor (floor): 3 + assert_eq!( + SafeMath::percentage(100, 333, RoundingMode::ProtocolFavor).unwrap(), + 3 + ); + + // Neutral (round half up): 3 + assert_eq!( + SafeMath::percentage(100, 333, RoundingMode::Neutral).unwrap(), + 3 + ); + + // User favor (ceil): 4 + assert_eq!( + SafeMath::percentage(100, 333, RoundingMode::UserFavor).unwrap(), + 4 + ); + } + + #[test] + fn test_percentage_edge_cases() { + // Zero amount + assert_eq!( + SafeMath::percentage(0, 1000, RoundingMode::Neutral).unwrap(), + 0 + ); + + // Invalid bps (> 10000) + assert_eq!( + SafeMath::percentage(1000, 10001, RoundingMode::Neutral), + Err(PrediFiError::InvalidFeeBps) + ); + + // Negative amount + assert_eq!( + SafeMath::percentage(-100, 1000, RoundingMode::Neutral), + Err(PrediFiError::ArithmeticError) + ); + + // Negative bps + assert_eq!( + SafeMath::percentage(1000, -100, RoundingMode::Neutral), + Err(PrediFiError::InvalidFeeBps) + ); + } + + #[test] + fn test_proportion_basic() { + // User staked 300 out of 1000, pool has 5000 + // (300 / 1000) * 5000 = 1500 + assert_eq!( + SafeMath::proportion(300, 1000, 5000, RoundingMode::Neutral).unwrap(), + 1500 + ); + + // User staked 1 out of 3, pool has 99 + // (1 / 3) * 99 = 33 + assert_eq!( + SafeMath::proportion(1, 3, 99, RoundingMode::Neutral).unwrap(), + 33 + ); + + // User staked all, gets all + assert_eq!( + SafeMath::proportion(1000, 1000, 5000, RoundingMode::Neutral).unwrap(), + 5000 + ); + + // User staked nothing, gets nothing + assert_eq!( + SafeMath::proportion(0, 1000, 5000, RoundingMode::Neutral).unwrap(), + 0 + ); + } + + #[test] + fn test_proportion_rounding() { + // (1 / 3) * 100 = 33.333... + // Protocol favor: 33 + assert_eq!( + SafeMath::proportion(1, 3, 100, RoundingMode::ProtocolFavor).unwrap(), + 33 + ); + + // Neutral: 33 (remainder 1 < half of 3) + // Actually: 100/3 = 33 remainder 1, half = 1, so 1 >= 1 rounds up to 34 + assert_eq!( + SafeMath::proportion(1, 3, 100, RoundingMode::Neutral).unwrap(), + 34 + ); + + // User favor: 34 + assert_eq!( + SafeMath::proportion(1, 3, 100, RoundingMode::UserFavor).unwrap(), + 34 + ); + + // (1 / 2) * 101 = 50.5 + // Neutral rounds up: 51 + assert_eq!( + SafeMath::proportion(1, 2, 101, RoundingMode::Neutral).unwrap(), + 51 + ); + } + + #[test] + fn test_proportion_edge_cases() { + // Zero denominator + assert_eq!( + SafeMath::proportion(100, 0, 1000, RoundingMode::Neutral), + Err(PrediFiError::ArithmeticError) + ); + + // Numerator > denominator + assert_eq!( + SafeMath::proportion(1001, 1000, 5000, RoundingMode::Neutral), + Err(PrediFiError::ArithmeticError) + ); + + // Negative values + assert_eq!( + SafeMath::proportion(-100, 1000, 5000, RoundingMode::Neutral), + Err(PrediFiError::ArithmeticError) + ); + + assert_eq!( + SafeMath::proportion(100, -1000, 5000, RoundingMode::Neutral), + Err(PrediFiError::ArithmeticError) + ); + } + + #[test] + fn test_multi_proportion_basic() { + let stakes = vec![300, 500, 200]; + let total = 1000; + let pool = 10000; + + let results = + SafeMath::multi_proportion(&stakes, total, pool, RoundingMode::ProtocolFavor).unwrap(); + + assert_eq!(results.len(), 3); + // First: (300/1000) * 10000 = 3000 + assert_eq!(results[0], 3000); + // Second: (500/1000) * 10000 = 5000 + assert_eq!(results[1], 5000); + // Third: gets remainder = 2000 + assert_eq!(results[2], 2000); + + // Sum should equal pool + let sum: i128 = results.iter().sum(); + assert_eq!(sum, pool); + } + + #[test] + fn test_multi_proportion_rounding_safety() { + // Test case where rounding could cause over-distribution + let stakes = vec![1, 1, 1]; + let total = 3; + let pool = 10; + + let results = + SafeMath::multi_proportion(&stakes, total, pool, RoundingMode::UserFavor).unwrap(); + + // With user favor rounding, first two would get 4 each (ceil(10/3)) + // But last gets remainder to prevent over-distribution + assert_eq!(results[0], 4); + assert_eq!(results[1], 4); + assert_eq!(results[2], 2); // Remainder, not 4 + + let sum: i128 = results.iter().sum(); + assert_eq!(sum, pool); + } + + #[test] + fn test_multi_proportion_edge_cases() { + // Empty stakes + let results = SafeMath::multi_proportion(&[], 1000, 5000, RoundingMode::Neutral).unwrap(); + assert_eq!(results.len(), 0); + + // Single stake + let stakes = vec![1000]; + let results = + SafeMath::multi_proportion(&stakes, 1000, 5000, RoundingMode::Neutral).unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0], 5000); + + // Zero total stake + let stakes = vec![100, 200]; + assert_eq!( + SafeMath::multi_proportion(&stakes, 0, 5000, RoundingMode::Neutral), + Err(PrediFiError::ArithmeticError) + ); + + // Negative stake + let stakes = vec![100, -200]; + assert_eq!( + SafeMath::multi_proportion(&stakes, 1000, 5000, RoundingMode::Neutral), + Err(PrediFiError::ArithmeticError) + ); + } + + #[test] + fn test_complex_payout_scenario() { + // Real-world scenario: 5 winners with different stakes + let stakes = vec![1_500_000, 3_200_000, 750_000, 2_100_000, 1_450_000]; + let total_stake = 9_000_000; + let pool_balance = 45_000_000; + + let payouts = + SafeMath::multi_proportion(&stakes, total_stake, pool_balance, RoundingMode::Neutral) + .unwrap(); + + // Verify sum doesn't exceed pool + let total_paid: i128 = payouts.iter().sum(); + assert!(total_paid <= pool_balance); + assert_eq!(total_paid, pool_balance); + + // Verify each payout is proportional (approximately) + for (i, &stake) in stakes.iter().enumerate() { + let expected = (stake as f64 / total_stake as f64) * pool_balance as f64; + let actual = payouts[i] as f64; + let diff = (expected - actual).abs(); + // Allow small rounding difference + assert!( + diff < 2.0, + "Payout {} differs too much: expected {}, got {}", + i, + expected, + actual + ); + } + } + + #[test] + fn test_safe_arithmetic() { + // Addition + assert_eq!(SafeMath::safe_add(100, 200).unwrap(), 300); + assert_eq!( + SafeMath::safe_add(i128::MAX, 1), + Err(PrediFiError::ArithmeticError) + ); + + // Subtraction + assert_eq!(SafeMath::safe_sub(200, 100).unwrap(), 100); + assert_eq!( + SafeMath::safe_sub(i128::MIN, 1), + Err(PrediFiError::ArithmeticError) + ); + + // Multiplication + assert_eq!(SafeMath::safe_mul(10, 20).unwrap(), 200); + assert_eq!( + SafeMath::safe_mul(i128::MAX, 2), + Err(PrediFiError::ArithmeticError) + ); + } + + #[test] + fn test_large_numbers() { + // Test with realistic token amounts (e.g., 7 decimal places) + let amount = 10_000_000_000_000; // 1M tokens with 7 decimals + let bps = 250; // 2.5% + let fee = SafeMath::percentage(amount, bps, RoundingMode::Neutral).unwrap(); + assert_eq!(fee, 250_000_000_000); // 25K tokens + + // Test proportion with large numbers + let user_stake = 5_000_000_000_000; + let total_stake = 20_000_000_000_000; + let pool = 100_000_000_000_000; + let payout = + SafeMath::proportion(user_stake, total_stake, pool, RoundingMode::Neutral).unwrap(); + assert_eq!(payout, 25_000_000_000_000); // 25% of pool + } +} diff --git a/contract/contracts/predifi-contract/src/safe_math_examples.rs b/contract/contracts/predifi-contract/src/safe_math_examples.rs new file mode 100644 index 0000000..f3fb94e --- /dev/null +++ b/contract/contracts/predifi-contract/src/safe_math_examples.rs @@ -0,0 +1,265 @@ +//! # Safe Math Usage Examples +//! +//! This module demonstrates how to use the SafeMath module for various +//! payout and fee calculation scenarios in the PrediFi contract. + +extern crate std; + +use crate::safe_math::{RoundingMode, SafeMath}; +use predifi_errors::PrediFiError; +use std::println; + +/// Example: Calculate protocol fee from a prediction amount +/// +/// When a user places a prediction, we need to deduct a protocol fee. +/// Using ProtocolFavor rounding ensures any fractional amounts stay in the pool. +#[test] +fn example_calculate_protocol_fee() { + let prediction_amount = 1_000_000_000; // 100 tokens (7 decimals) + let fee_bps = 250; // 2.5% + + // Calculate fee with protocol-favoring rounding + let fee = SafeMath::percentage(prediction_amount, fee_bps, RoundingMode::ProtocolFavor) + .expect("Fee calculation failed"); + + let net_amount = SafeMath::safe_sub(prediction_amount, fee).expect("Subtraction failed"); + + assert_eq!(fee, 25_000_000); // 2.5 tokens + assert_eq!(net_amount, 975_000_000); // 97.5 tokens + + println!("Prediction: {}", prediction_amount); + println!("Fee (2.5%): {}", fee); + println!("Net stake: {}", net_amount); +} + +/// Example: Calculate winner payout proportionally +/// +/// When a pool resolves, winners get paid proportionally to their stake +/// in the winning outcome. +#[test] +fn example_calculate_winner_payout() { + let user_stake = 500_000_000; // User staked 50 tokens + let winning_outcome_total = 2_000_000_000; // Total winning stake: 200 tokens + let pool_balance = 10_000_000_000; // Pool has 1000 tokens to distribute + + // Calculate user's proportional payout + // Using Neutral rounding for fairness + let payout = SafeMath::proportion( + user_stake, + winning_outcome_total, + pool_balance, + RoundingMode::Neutral, + ) + .expect("Payout calculation failed"); + + // User staked 25% of winning side, gets 25% of pool + assert_eq!(payout, 2_500_000_000); // 250 tokens + + println!("User stake: {}", user_stake); + println!("Total winning stake: {}", winning_outcome_total); + println!("Pool balance: {}", pool_balance); + println!("User payout: {}", payout); +} + +/// Example: Handle edge case where user is sole winner +#[test] +fn example_sole_winner() { + let user_stake = 100_000_000; // User staked 10 tokens + let winning_outcome_total = 100_000_000; // Only winner + let pool_balance = 5_000_000_000; // Pool has 500 tokens + + let payout = SafeMath::proportion( + user_stake, + winning_outcome_total, + pool_balance, + RoundingMode::Neutral, + ) + .expect("Payout calculation failed"); + + // User gets entire pool + assert_eq!(payout, pool_balance); + + println!("Sole winner gets entire pool: {}", payout); +} + +/// Example: Prevent division by zero +#[test] +fn example_zero_stake_protection() { + let user_stake = 100_000_000; + let winning_outcome_total = 0; // No winning stakes (shouldn't happen, but protected) + let pool_balance = 5_000_000_000; + + let result = SafeMath::proportion( + user_stake, + winning_outcome_total, + pool_balance, + RoundingMode::Neutral, + ); + + // Should return error, not panic + assert_eq!(result, Err(PrediFiError::ArithmeticError)); + + println!("Division by zero safely caught"); +} + +/// Example: Calculate fee and verify it doesn't exceed amount +#[test] +fn example_fee_validation() { + let amount = 1000; + let fee_bps = 10000; // 100% + + let fee = SafeMath::percentage(amount, fee_bps, RoundingMode::Neutral) + .expect("Fee calculation failed"); + + assert_eq!(fee, amount); // Fee equals amount at 100% + + // Verify fee doesn't exceed amount + assert!(fee <= amount); + + // Invalid fee (> 100%) is rejected + let invalid_result = SafeMath::percentage(amount, 10001, RoundingMode::Neutral); + assert_eq!(invalid_result, Err(PrediFiError::InvalidFeeBps)); + + println!("Fee validation working correctly"); +} + +/// Example: Rounding mode comparison +#[test] +fn example_rounding_modes() { + let user_stake = 333; // 33.3% of total + let total_stake = 1000; + let pool_balance = 100; + + // Protocol favor: rounds down (33) + let protocol_payout = SafeMath::proportion( + user_stake, + total_stake, + pool_balance, + RoundingMode::ProtocolFavor, + ) + .unwrap(); + + // Neutral: rounds to nearest (33) + let neutral_payout = + SafeMath::proportion(user_stake, total_stake, pool_balance, RoundingMode::Neutral).unwrap(); + + // User favor: rounds up (34) + let user_payout = SafeMath::proportion( + user_stake, + total_stake, + pool_balance, + RoundingMode::UserFavor, + ) + .unwrap(); + + println!("Protocol favor: {}", protocol_payout); // 33 + println!("Neutral: {}", neutral_payout); // 33 + println!("User favor: {}", user_payout); // 34 + + // For production, use ProtocolFavor or Neutral to prevent over-distribution + assert!(protocol_payout <= neutral_payout); + assert!(neutral_payout <= user_payout); +} + +/// Example: Safe arithmetic operations +#[test] +fn example_safe_arithmetic() { + let pool_balance = 1_000_000_000; + let payout = 250_000_000; + + // Safe subtraction + let remaining = SafeMath::safe_sub(pool_balance, payout).expect("Subtraction failed"); + assert_eq!(remaining, 750_000_000); + + // Safe addition + let new_stake = 100_000_000; + let updated_total = SafeMath::safe_add(pool_balance, new_stake).expect("Addition failed"); + assert_eq!(updated_total, 1_100_000_000); + + // Overflow protection + let overflow_result = SafeMath::safe_add(i128::MAX, 1); + assert_eq!(overflow_result, Err(PrediFiError::ArithmeticError)); + + println!("Safe arithmetic prevents overflow/underflow"); +} + +/// Example: Real-world payout scenario with multiple winners +#[test] +fn example_realistic_payout_scenario() { + // Pool setup + let total_pool = 10_000_000_000; // 1000 tokens + let fee_bps = 200; // 2% protocol fee + + // Calculate protocol fee + let protocol_fee = SafeMath::percentage(total_pool, fee_bps, RoundingMode::ProtocolFavor) + .expect("Fee calculation failed"); + + // Remaining for winners + let payout_pool = SafeMath::safe_sub(total_pool, protocol_fee).expect("Subtraction failed"); + + // Winner stakes on the winning outcome + let winner1_stake = 300_000_000; // 30 tokens + let winner2_stake = 500_000_000; // 50 tokens + let winner3_stake = 200_000_000; // 20 tokens + let total_winning_stake = 1_000_000_000; // 100 tokens total + + // Calculate each winner's payout + let payout1 = SafeMath::proportion( + winner1_stake, + total_winning_stake, + payout_pool, + RoundingMode::Neutral, + ) + .expect("Payout 1 failed"); + + let payout2 = SafeMath::proportion( + winner2_stake, + total_winning_stake, + payout_pool, + RoundingMode::Neutral, + ) + .expect("Payout 2 failed"); + + let payout3 = SafeMath::proportion( + winner3_stake, + total_winning_stake, + payout_pool, + RoundingMode::Neutral, + ) + .expect("Payout 3 failed"); + + println!("Total pool: {}", total_pool); + println!("Protocol fee (2%): {}", protocol_fee); + println!("Payout pool: {}", payout_pool); + println!("Winner 1 (30%): {}", payout1); + println!("Winner 2 (50%): {}", payout2); + println!("Winner 3 (20%): {}", payout3); + + // Verify total payouts don't exceed payout pool + let total_paid = SafeMath::safe_add(payout1, payout2) + .and_then(|sum| SafeMath::safe_add(sum, payout3)) + .expect("Sum failed"); + + assert!(total_paid <= payout_pool); + println!("Total paid: {} (safe)", total_paid); +} + +/// Example: Handling very small amounts (dust) +#[test] +fn example_dust_handling() { + let tiny_amount = 10; // Very small amount + let fee_bps = 100; // 1% + + // With protocol favor, small fees round down to 0 + let fee = SafeMath::percentage(tiny_amount, fee_bps, RoundingMode::ProtocolFavor) + .expect("Fee calculation failed"); + + // 1% of 10 = 0.1, rounds down to 0 + assert_eq!(fee, 0); + + // User keeps full amount when fee rounds to 0 + let net = SafeMath::safe_sub(tiny_amount, fee).expect("Subtraction failed"); + assert_eq!(net, tiny_amount); + + println!("Dust amounts handled gracefully"); +}