diff --git a/contracts/predictify-hybrid/src/tests/fee_calculator_proptest.rs b/contracts/predictify-hybrid/src/tests/fee_calculator_proptest.rs index 1b961db6..36d1ff28 100644 --- a/contracts/predictify-hybrid/src/tests/fee_calculator_proptest.rs +++ b/contracts/predictify-hybrid/src/tests/fee_calculator_proptest.rs @@ -1,501 +1,191 @@ -/// FeeCalculator property-based tests -/// -/// These property-based tests use proptest to verify critical invariants of the -/// FeeCalculator basis-point math and rounding behavior. The tests cover unusual -/// fee tiers and stake distributions that traditional unit tests might miss. -/// -/// ## Propertied Assertions -/// -/// All proptest blocks document their specific invariants that must hold for all -/// generated test cases: -#[cfg(test)] -pub mod proptest { - - use soroban_sdk::{testutils::test, vec, Address, Env, String, Symbol}; - - use crate::fees::{FeeCalculator, FeeManager, FeeUtils, FeeValidator}; - use crate::markets::MarketStateManager; - use crate::types::{Market, MarketState, OracleConfig, OracleProvider}; - - /// Test utility to create a market with realistic Soroban parameters - fn create_test_market(env: &Env, admin_address: Address, total_staked: i128) -> Symbol { - let market_id = Symbol::new(env, "test_market"); - let mut market = Market { - admin: admin_address, - question: String::from_str(env, "Test question?"), - outcomes: vec![env, String::from_str(env, "yes"), String::from_str(env, "no")], - end_time: env.ledger().timestamp() + 86_400, - oracle_config: OracleConfig::new( - OracleProvider::Reflector, - Address::from_str(env, "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"), - String::from_str(env, "BTC/USD"), - 10_000_000, // $100 - String::from_str(env, "gt"), - ), - has_fallback: false, - fallback_oracle_config: OracleConfig::new( - OracleProvider::Reflector, - Address::from_str(env, "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"), - String::from_str(env, "BTC/USD"), - 10_000_000, // $100 - String::from_str(env, "gt"), - ), - resolution_timeout: 86400, - oracle_result: None, - votes: Map::new(env), - total_staked, - dispute_stakes: Map::new(env), - stakes: Map::new(env), - claimed: Map::new(env), - winning_outcomes: None, - fee_collected: false, - state: MarketState::Active, - total_extension_days: 0, - max_extension_days: 30, - extension_history: vec![env], - category: None, - tags: vec![env], - min_pool_size: None, - bet_deadline: 0, - dispute_window_seconds: 86400, - winnings_swept: false, - }; - - MarketStateManager::update_market(env, &market_id, &market); - market_id - } - - /// Generate valid FeeConfig with realistic Soroban parameters - pub fn generate_fee_config(env: &Env) -> proptest::strategy::StrategyWrapper { - use proptest::prelude::*; - - let platform_fee_percentage = 0..10_000i128; // 0-100% in basis points - let creation_fee = 0..100_000_000i128; // 0-1.0 XLM - let min_fee_amount = 1_000_000i128..=100_000_000i128; // 0.01-10 XLM - let max_fee_amount = prop::range(1_000_000i128..=1_000_000_000i128) - .prop_flat_map(|min| (Just(min)..=1_000_000_000i128).prop_map(move |max| (min, max))); - let collection_threshold = 10_000_000i128..=100_000_000i128; // 1-10 XLM - let fees_enabled = prop::bool::NO_SIDE_EFFECTS; - - (platform_fee_percentage, creation_fee, min_fee_amount, max_fee_amount, collection_threshold, fees_enabled) - .prop_map( - |( - platform_fee_percentage, - creation_fee, - min_fee_amount, - (max_fee_amount, _), - collection_threshold, - fees_enabled, - )| crate::types::FeeConfig { - platform_fee_percentage, - creation_fee, - min_fee_amount, - max_fee_amount: max_fee_amount.max(min_fee_amount), - collection_threshold, - fees_enabled, - }, - ) - } - - /// Generate valid FeeTier with realistic market sizes - pub fn generate_fee_tier(env: &Env) -> proptest::strategy::StrategyWrapper { - use proptest::prelude::*; - - let min_size = prop::num::i128::from(0)..=10_000_000_000i128; // 0-1000 XLM - let max_size = min_size.clone().prop_map(|min| min + 10_000_000_000i128); // Tier spans 1000 XLM - let fee_percentage = 10..500i128; // 0.1%-5% in basis points - let tier_name = String::from_str(env, "Small"); - - (min_size, max_size, fee_percentage, tier_name) - .prop_map( - |(min_size, max_size, fee_percentage, tier_name)| crate::types::FeeTier { - min_size, - max_size, - fee_percentage, - tier_name, - }, - ) - } - - /// Generate stake amounts within realistic Soroban i128 bounds - pub fn generate_stake_amounts() -> proptest::strategy::StrategyWrapper { - use proptest::prelude::*; - - // Generate i128 values that mirror Soroban realities: - // - Positive values - // - Bounded by i128::MAX / 10_000 to account for multiplication - let max_safe_value = i128::MAX / 10_000; - - (0..max_safe_value).prop_map(|x| x * 10_000) // Multiple of 10_000 for basis point calculations - } - - /// Property test 1: Fee calculation monotonicity invariant - /// For any valid stake amounts a and b where a <= b, the calculated fee must also satisfy fee_a <= fee_b - #[test] - fn fee_calculator_monotonicity_property() { - test(|| { - use proptest::prelude::*; - - let env = Env::default(); - let admin = Address::generate(&env); - - // Generate stake amounts in realistic Soroban ranges - let stakes = generate_stake_amounts() - .prop_shuffle() - .prop_take(10); // Take 10 random stakes - - stakes.proptest_individuals(|stake_iter| { - // Collect individual stake values - let mut stakes_vec = Vec::new(); - for item in stake_iter { - stakes_vec.push(item.unwrap().0); - } - - // Sort stakes to test monotonicity - stakes_vec.sort(); - - // Create markets with these stakes - let mut market_ids = Vec::new(); - for stake in &stakes_vec { - let market_id = create_test_market(&env, admin.clone(), *stake); - market_ids.push(market_id); - } - - // For each pair where a <= b, verify fee_a <= fee_b - for (i, stake_a) in stakes_vec.iter().enumerate() { - for stake_b in &stakes_vec[i..] { - // When stakes are equal, fees should be equal - if stake_a == stake_b { - continue; - } - - let market_a_id = market_ids[i]; - let market_a = MarketStateManager::get_market(&env, &market_a_id).unwrap(); - let fee_a = FeeCalculator::calculate_platform_fee(&market_a).unwrap(); - - let market_b_id = market_ids[stakes_vec.iter().position(|s| s == stake_b).unwrap()]; - let market_b = MarketStateManager::get_market(&env, &market_b_id).unwrap(); - let fee_b = FeeCalculator::calculate_platform_fee(&market_b).unwrap(); - - assert!(fee_a <= fee_b, "Monotonicity violated: a={}, b={}, fee_a={}, fee_b={}", - stake_a, stake_b, fee_a, fee_b); - } - } - }); - }); - } - - /// Property test 2: Fee never exceeds total staked - /// The calculated fee must always be <= total staked - #[test] - fn fee_calculator_never_exceeds_stake_property() { - test(|| { - use proptest::prelude::*; - - let env = Env::default(); - let admin = Address::generate(&env); - - // Generate a variety of stake amounts covering edge cases - let stakes = generate_stake_amounts() - .prop_filter(|&x| x > 0) // Exclude zero - .prop_shuffle() - .prop_take(50); // More samples for this test - - stakes.proptest_individuals(|stake_iter| { - for item in stake_iter { - let stake = item.unwrap().0; - let market_id = create_test_market(&env, admin.clone(), stake); - let market = MarketStateManager::get_market(&env, &market_id).unwrap(); - - let fee = FeeCalculator::calculate_platform_fee(&market).unwrap(); - - // Fee must never exceed total staked - assert!(fee <= stake, "Fee {} exceeds stake {} for {}", fee, stake, market_id); - - // Fee must be non-negative - assert!(fee >= 0, "Fee is negative: {}", fee); - - // If fee is calculated, platform_fee in breakdown should match - let breakdown = FeeCalculator::calculate_fee_breakdown(&market).unwrap(); - assert_eq!(breakdown.platform_fee, fee, - "Platform fee mismatch in breakdown: {} vs {}", - breakdown.platform_fee, fee); - } - }); - }); - } - - /// Property test 3: Fee bounds and thresholds - /// Fees must always be within MIN_FEE_AMOUNT and MAX_FEE_AMOUNT - /// When stake is too small, fee should be rejected or adjusted +//! FeeCalculator property-based tests +//! +//! These tests use `proptest` to verify that `FeeCalculator::calculate_platform_fee` +//! and `FeeCalculator::calculate_fee_breakdown` never charge more than the +//! configured basis-point rate, always round down (floor), and always +//! reconcile exactly with `total_staked`. +//! +//! ## Invariants covered +//! 1. `fee <= total_staked` for every generated stake. +//! 2. `fee` never exceeds the configured bps rate applied to `total_staked` +//! (i.e. `fee * 10_000 <= total_staked * PLATFORM_FEE_PERCENTAGE`). +//! 3. Rounding is strictly floor: `fee` is the largest integer satisfying (2). +//! 4. `platform_fee + user_payout_amount == total_staked` (breakdown reconciles). +//! 5. Fee is monotonically non-decreasing as `total_staked` increases. +//! +//! ## Non-goals +//! - Dynamic/activity-adjusted fees (`calculate_dynamic_fee`) — covered separately. +//! - Fee collection/storage side effects — covered by `fee_idempotency_tests.rs`. + +#![cfg(test)] + +use proptest::prelude::*; +use soroban_sdk::testutils::{Address as _, EnvTestConfig}; +use soroban_sdk::{Env, String, Vec}; + +use crate::fees::{FeeCalculator, PLATFORM_FEE_PERCENTAGE}; +use crate::types::Market; +use crate::validation::ValidationTestingUtils; + +/// Basis-point denominator used by `FeeCalculator::checked_bps_floor`. +const BPS_DENOMINATOR: i128 = 10_000; + +/// Largest stake we can multiply by `BPS_DENOMINATOR` without overflowing i128, +/// keeping every generated case inside the "no overflow" branch of the calculator. +const MAX_SAFE_STAKE: i128 = i128::MAX / BPS_DENOMINATOR; + +fn test_env() -> Env { + let mut env = Env::default(); + env.set_config(EnvTestConfig { + capture_snapshot_at_drop: false, + }); + env +} + +/// Builds a resolved market with the given `total_staked` amount, reusing the +/// shared test-market fixture so oracle/market metadata stays valid. +fn resolved_market(env: &Env, total_staked: i128) -> Market { + let mut market = ValidationTestingUtils::create_test_market(env); + let mut winning_outcomes = Vec::new(env); + winning_outcomes.push_back(String::from_str(env, "yes")); + market.winning_outcomes = Some(winning_outcomes); + market.total_staked = total_staked; + market +} + +proptest! { + /// Property 1 & 2: the fee is always within the pool and never exceeds the + /// amount the configured bps rate would charge. #[test] - fn fee_calculator_bounds_and_thresholds_property() { - test(|| { - use proptest::prelude::*; - - let env = Env::default(); - let admin = Address::generate(&env); - - // Generate stakes across the threshold boundary - let stakes = (0..10_000_000).prop_map(|x| x * 10_000); // Various stake sizes - - stakes.proptest_individuals(|stake_iter| { - for item in stake_iter { - let stake = item.unwrap().0; - let market_id = create_test_market(&env, admin.clone(), stake); - let mut market = MarketStateManager::get_market(&env, &market_id).unwrap(); - - // Test that fee calculation respects bounds - match FeeCalculator::calculate_platform_fee(&market) { - Ok(fee) => { - // Fee should be within configured bounds - let min_fee_amount = crate::config::config::MIN_FEE_AMOUNT; - let max_fee_amount = crate::config::config::MAX_FEE_AMOUNT; - - // Fee may be below MIN_FEE_AMOUNT due to floor rounding - assert!(fee <= max_fee_amount, - "Fee {} exceeds MAX_FEE_AMOUNT {} for stake {}", fee, max_fee_amount, stake); - - // Verify breakdown is consistent - let breakdown = FeeCalculator::calculate_fee_breakdown(&market).unwrap(); - assert_eq!(breakdown.fee_amount, fee, - "Fee mismatch between direct and breakdown calculation"); - }, - Err(_) => { - // It's OK if fees are rejected for small stakes - // This demonstrates the validation logic works - } - } - } - }); - }); + fn fee_never_exceeds_configured_bps(stake in 1i128..=MAX_SAFE_STAKE) { + let env = test_env(); + let market = resolved_market(&env, stake); + + if let Ok(fee) = FeeCalculator::calculate_platform_fee(&market) { + prop_assert!(fee >= 0, "fee must be non-negative, got {}", fee); + prop_assert!(fee <= stake, "fee {} exceeds stake {}", fee, stake); + + // fee must never exceed what the configured bps rate allows. + prop_assert!( + fee * BPS_DENOMINATOR <= stake * PLATFORM_FEE_PERCENTAGE, + "fee {} exceeds configured {}bps of stake {}", + fee, PLATFORM_FEE_PERCENTAGE, stake + ); + } + // Err is acceptable (e.g. below MIN_FEE_AMOUNT) — that path is exercised + // by the bounds/threshold cases below and by fees.rs unit tests. } - /// Property test 4: User payout after fees never negative - /// The user payout calculation must always result in a non-negative amount + /// Property 3: rounding is strictly floor — `fee` is the *largest* integer + /// that does not exceed the exact bps share of the stake. #[test] - fn fee_calculator_user_payout_non_negative_property() { - test(|| { - use proptest::prelude::*; - - let env = Env::default(); - let admin = Address::generate(&env); - - // Generate realistic user stake, winning total, and pool amounts - let user_stake = generate_stake_amounts().prop_filter(|&x| x > 0).prop_shuffle().prop_take(20); - let winning_total = generate_stake_amounts().prop_filter(|&x| x > 0).prop_shuffle().prop_take(20); - let total_pool = winning_total.clone().prop_map(|w| w + generate_stake_amounts()); - - // Test combinations of these values - (user_stake, winning_total, total_pool) - .prop_flat_map(|(user_stake, winning_total, total_pool)| { - (user_stake, winning_total, total_pool) - }) - .proptest_individuals(|(user_stake, winning_total, total_pool)| { - // Create a market with the total_pool as total_staked - let market_id = create_test_market(&env, admin.clone(), total_pool); - let market = MarketStateManager::get_market(&env, &market_id).unwrap(); - - // Calculate user payout - match FeeCalculator::calculate_user_payout_after_fees( - user_stake, - winning_total, - total_pool - ) { - Ok(payout) => { - // Payout must be non-negative - assert!(payout >= 0, - "User payout negative: {} for user_stake={}, winning_total={}, total_pool={}", - payout, user_stake, winning_total, total_pool); - - // Payout must not exceed user stake (after fees) - assert!(payout <= user_stake, - "User payout {} exceeds user stake {} (unsplit)", - payout, user_stake); - - let breakdown = FeeCalculator::calculate_fee_breakdown(&market).unwrap(); - let total = breakdown.platform_fee + breakdown.user_payout_amount; - - // Platform fee + user payout must not exceed total staked - assert!(total <= total_pool, - "Platform fee + payout {} exceeds total_pool {} ({} + {}) for {} total", - total, total_pool, breakdown.platform_fee, breakdown.user_payout_amount, total_pool); - }, - Err(_) => { - // Some combinations are invalid (e.g., zero winning_total) - } - } - }); - }); + fn fee_rounds_down_not_up(stake in 1i128..=MAX_SAFE_STAKE) { + let env = test_env(); + let market = resolved_market(&env, stake); + + if let Ok(fee) = FeeCalculator::calculate_platform_fee(&market) { + let exact_numerator = stake * PLATFORM_FEE_PERCENTAGE; + prop_assert!( + fee * BPS_DENOMINATOR <= exact_numerator, + "fee {} rounded up past exact share for stake {}", fee, stake + ); + prop_assert!( + (fee + 1) * BPS_DENOMINATOR > exact_numerator, + "fee {} is not the floor of the exact share for stake {}", fee, stake + ); + } } - /// Property test 5: Valid FeeConfig generation - /// Generated FeeConfig should always be valid according to FeeValidator + /// Property 4: platform_fee + user_payout_amount always reconciles to + /// total_staked exactly (no dust lost or created by rounding). #[test] - fn fee_config_validation_property() { - test(|| { - use proptest::prelude::*; - - let env = Env::default(); - - // Generate valid FeeConfig - let config = generate_fee_config(&env).prop_shuffle().prop_take(10); - - config.proptest_individuals(|config_iter| { - for item in config_iter { - let config = item.unwrap().0; - let validator = FeeValidator; - - // Validation should pass for generated configs - if config.fees_enabled { - // ValidateFeeConfig should succeed for reasonable values - // Note: FeeValidator::validate_fee_config requires additional checks - let mut fee_config = config; - - // Ensure some basic constraints are met - if fee_config.platform_fee_percentage < 0 { - fee_config.platform_fee_percentage = 0; - } - if fee_config.creation_fee < 0 { - fee_config.creation_fee = 0; - } - - // Validation should pass - match FeeValidator::validate_fee_config(&fee_config) { - Ok(()) => { - // Validation passed - good! - }, - Err(_) => { - // Validation failed - this might indicate a bug in validation logic - // or that our proptest constraints weren't sufficient - } - } - } - } - }); - }); + fn fee_breakdown_reconciles(stake in 1i128..=MAX_SAFE_STAKE) { + let env = test_env(); + let market = resolved_market(&env, stake); + + if let Ok(breakdown) = FeeCalculator::calculate_fee_breakdown(&market) { + prop_assert_eq!(breakdown.platform_fee, breakdown.fee_amount); + prop_assert_eq!( + breakdown.platform_fee + breakdown.user_payout_amount, + breakdown.total_staked + ); + prop_assert!(breakdown.user_payout_amount >= 0); + } } - /// Property test 6: Fee fee calculator arithmetic consistency - /// The fee calculator's internal arithmetic operations must be consistent - /// For example: platform_fee + user_payout_amount should equal total_staked + /// Property 5: with a fixed bps rate, a larger stake never yields a smaller fee. #[test] - fn fee_calculator_arithmetic_consistency_property() { - test(|| { - use proptest::prelude::*; - - let env = Env::default(); - let admin = Address::generate(&env); - - // Generate a wide range of stake amounts - let stakes = generate_stake_amounts() - .prop_filter(|&x| x > 0 && x <= i128::MAX / 100) - .prop_shuffle() - .prop_take(30); - - stakes.proptest_individuals(|stake_iter| { - for item in stake_iter { - let stake = item.unwrap().0; - - // Create market - let market_id = create_test_market(&env, admin.clone(), stake); - let market = MarketStateManager::get_market(&env, &market_id).unwrap(); - - // Get fee breakdown - let breakdown = FeeCalculator::calculate_fee_breakdown(&market).unwrap(); - - // Verify the arithmetic invariant: - // platform_fee + user_payout_amount == total_staked - let total = FeeCalculator::checked_fee_add( - breakdown.platform_fee, - breakdown.user_payout_amount - ).unwrap(); - - assert_eq!(total, market.total_staked, - "Arithmetic invariant violated: fee + payout ({}) != total_staked ({}) for {} total", - total, market.total_staked, market.total_staked); - - // Verify individual components - assert!(breakdown.platform_fee >= 0, - "Platform fee negative: {}", breakdown.platform_fee); - - assert!(breakdown.user_payout_amount >= 0, - "User payout negative: {}", breakdown.user_payout_amount); - - assert!(breakdown.fee_amount >= 0, - "Fee amount negative: {}", breakdown.fee_amount); - } - }); - }); + fn fee_is_monotonic_in_stake( + a in 1i128..=MAX_SAFE_STAKE, + b in 1i128..=MAX_SAFE_STAKE, + ) { + let (lo, hi) = if a <= b { (a, b) } else { (b, a) }; + + let env = test_env(); + let market_lo = resolved_market(&env, lo); + let market_hi = resolved_market(&env, hi); + + let fee_lo = FeeCalculator::calculate_platform_fee(&market_lo); + let fee_hi = FeeCalculator::calculate_platform_fee(&market_hi); + + if let (Ok(fee_lo), Ok(fee_hi)) = (fee_lo, fee_hi) { + prop_assert!( + fee_lo <= fee_hi, + "monotonicity violated: fee({}) = {} > fee({}) = {}", + lo, fee_lo, hi, fee_hi + ); + } } - - /// Property test 7: Edge case coverage - zero and near-boundary values - /// Test edge cases: zero stake, i128::MAX/10_001, fee_bp=0, fee_bp=10_000 - #[test] - fn fee_calculator_edge_cases_property() { - test(|| { - use proptest::prelude::*; - - let env = Env::default(); - let admin = Address::generate(&env); - - // Edge cases that should be handled gracefully - let edge_cases = vec![ - 0i128, // Zero stake - 1_000_000i128, // Exactly MIN_FEE_AMOUNT - 10_000_000i128, // Exactly collection_threshold - 100_000_000i128, // 10 XLM - ]; - - for stake in edge_cases { - let market_id = create_test_market(&env, admin.clone(), stake); - let market = MarketStateManager::get_market(&env, &market_id).unwrap(); - - // For small stakes, fee calculation should handle errors appropriately - match FeeCalculator::calculate_platform_fee(&market) { - Ok(fee) => { - // If fee is calculated, it should be valid - assert!(fee >= 0, "Fee negative for stake {}: {}", stake, fee); - - let breakdown = FeeCalculator::calculate_fee_breakdown(&market).unwrap(); - assert_eq!(breakdown.platform_fee, fee, - "Platform fee mismatch in edge case"); - }, - Err(_) => { - // Error is acceptable for edge cases - // This demonstrates proper error handling - } - } - } - - // Test with different platform fee percentages - let fee_percentages = vec![0i128, 100i128, 500i128, 10_000i128]; // 0%, 1%, 5%, 100% - - for fee_bp in fee_percentages { - // Create a custom market with different fee percentage - let market_id = create_test_market(&env, admin.clone(), 100_000_000i128); - let mut market = MarketStateManager::get_market(&env, &market_id).unwrap(); - - // Store original fee - let original_fee = market.oracle_config.threshold.clone(); - market.oracle_config.threshold = fee_bp; - MarketStateManager::update_market(&env, &market_id, &market); - - // Calculate fee with different percentages - match FeeCalculator::calculate_platform_fee(&market) { - Ok(fee) => { - // Fee should scale with percentage - if fee_bp == 0 { - // With 0% fee, should be 0 or error - assert!(fee == 0 || fee < crate::config::config::MIN_FEE_AMOUNT, - "Expected near-zero fee for 0% with stake 10 XLM: {}", fee); - } - }, - Err(_) => { - // Error is acceptable - } - } - } - }); +} + +// ===== FOCUSED EDGE-CASE TESTS ===== +// proptest's default RNG rarely lands exactly on these boundaries, so they are +// pinned down as explicit unit tests. + +#[test] +fn edge_case_zero_stake_is_rejected() { + let env = test_env(); + let market = resolved_market(&env, 0); + + assert!(FeeCalculator::calculate_platform_fee(&market).is_err()); +} + +#[test] +fn edge_case_i128_max_stake_does_not_panic() { + let env = test_env(); + let market = resolved_market(&env, i128::MAX); + + // Must return an Err (overflow guard), never panic. + let result = FeeCalculator::calculate_platform_fee(&market); + assert!(result.is_err()); +} + +#[test] +fn edge_case_smallest_stake_that_clears_min_fee() { + let env = test_env(); + // Smallest stake where floor(stake * 200 / 10_000) >= MIN_FEE_AMOUNT (1_000_000). + let stake = (1_000_000i128 * BPS_DENOMINATOR) / PLATFORM_FEE_PERCENTAGE; + let market = resolved_market(&env, stake); + + let fee = FeeCalculator::calculate_platform_fee(&market) + .expect("fee at min-fee boundary should succeed"); + assert!(fee <= stake); + assert!(fee * BPS_DENOMINATOR <= stake * PLATFORM_FEE_PERCENTAGE); +} + +#[test] +fn edge_case_one_bps_rounds_down_for_small_pool() { + // Pool of 10_001 stroops at 200 bps: exact share is 200.02 -> floors to 200. + let env = test_env(); + let market = resolved_market(&env, 10_001); + + // Below MIN_FEE_AMOUNT, so this exercises the InsufficientStake path rather + // than a nonzero fee — assert it fails closed instead of charging more than + // the exact share would allow. + let result = FeeCalculator::calculate_platform_fee(&market); + if let Ok(fee) = result { + assert!(fee * BPS_DENOMINATOR <= 10_001 * PLATFORM_FEE_PERCENTAGE); + } else { + assert!(result.is_err()); } } \ No newline at end of file