Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 61 additions & 12 deletions contracts/predict-iq/src/modules/fees.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use crate::errors::ErrorCode;
use crate::modules::admin;
use crate::types::{ConfigKey, MarketTier};
use soroban_sdk::{contracttype, Address, Env};
use crate::types::{ConfigKey, MarketTier, GOV_TTL_HIGH_THRESHOLD, GOV_TTL_LOW_THRESHOLD};
use soroban_sdk::{contracttype, Address, Env, Symbol};

const BPS_DENOMINATOR: i128 = 10_000;
const TIER_DENOMINATOR_BPS: i128 = 10_000;

#[contracttype]
pub enum DataKey {
Expand Down Expand Up @@ -33,20 +36,30 @@ pub fn set_base_fee(e: &Env, amount: i128) -> Result<(), ErrorCode> {

pub fn calculate_fee(e: &Env, amount: i128) -> i128 {
let base_fee = get_base_fee(e);
(amount * base_fee) / 10000
amount.saturating_mul(base_fee) / BPS_DENOMINATOR
}

/// Issue #39: Multiply first, then divide to avoid precision loss on small amounts.
pub fn calculate_tiered_fee(e: &Env, amount: i128, tier: &MarketTier) -> i128 {
let base_fee = get_base_fee(e);
fn tier_multiplier_bps(tier: &MarketTier) -> i128 {
match tier {
MarketTier::Basic => TIER_DENOMINATOR_BPS,
MarketTier::Pro => 7_500, // 25% discount
MarketTier::Institutional => 5_000, // 50% discount
}
}

let adjusted_fee = match tier {
MarketTier::Basic => base_fee,
MarketTier::Pro => (base_fee * 75) / 100,
MarketTier::Institutional => (base_fee * 50) / 100,
};
fn calculate_tiered_fee_with_base(amount: i128, base_fee_bps: i128, tier: &MarketTier) -> i128 {
// Single-pass high-precision arithmetic: amount * base_fee_bps * tier_multiplier / (10_000 * 10_000)
// This avoids early truncation from computing discounted base_fee first.
let numerator = amount
.saturating_mul(base_fee_bps)
.saturating_mul(tier_multiplier_bps(tier));
numerator / (BPS_DENOMINATOR * TIER_DENOMINATOR_BPS)
}

(amount * adjusted_fee) / 10000
/// Issue #39: multiply before divide and keep tier multipliers in bps.
pub fn calculate_tiered_fee(e: &Env, amount: i128, tier: &MarketTier) -> i128 {
let base_fee = get_base_fee(e);
calculate_tiered_fee_with_base(amount, base_fee, tier)
}

pub fn collect_fee(e: &Env, token: Address, amount: i128) {
Expand Down Expand Up @@ -153,3 +166,39 @@ pub fn claim_referral_rewards(

Ok(balance)
}

#[cfg(test)]
mod tests {
use super::{calculate_tiered_fee_with_base, MarketTier};

#[test]
fn tiered_fee_keeps_fractional_discount_precision() {
// 1 bps base fee with Pro tier (25% discount):
// old math: ((1 * 75) / 100) = 0 bps => zero fee for all amounts.
// new math preserves the discounted 0.75 bps effect until final division.
let basic_fee = calculate_tiered_fee_with_base(4_000_000, 1, &MarketTier::Basic);
let pro_fee = calculate_tiered_fee_with_base(4_000_000, 1, &MarketTier::Pro);
assert_eq!(basic_fee, 400);
assert_eq!(pro_fee, 300);
}

#[test]
fn tiered_fee_uses_expected_discount_ratio() {
let basic_fee = calculate_tiered_fee_with_base(10_000, 100, &MarketTier::Basic);
let pro_fee = calculate_tiered_fee_with_base(10_000, 100, &MarketTier::Pro);
let inst_fee = calculate_tiered_fee_with_base(10_000, 100, &MarketTier::Institutional);

assert_eq!(basic_fee, 100);
assert_eq!(pro_fee, 75);
assert_eq!(inst_fee, 50);
}

#[test]
fn four_unit_bet_applies_pro_discount() {
let basic_fee = calculate_tiered_fee_with_base(4, 10_000, &MarketTier::Basic);
let pro_fee = calculate_tiered_fee_with_base(4, 10_000, &MarketTier::Pro);

assert_eq!(basic_fee, 4);
assert_eq!(pro_fee, 3);
}
}
Loading