From e7502da9c81f0b4ba511bb1469968cfd9ec11791 Mon Sep 17 00:00:00 2001 From: DavisVT Date: Sun, 31 May 2026 06:50:00 +0100 Subject: [PATCH] Add token amount normalization for issue #551 - Add normalize_amount and denormalize_amount functions in tokens.rs - Document canonical 7-decimal scale at top of tokens.rs - Wire normalize_amount into minimum pool checks in resolution.rs - Add comprehensive tests for normalization functions --- contracts/predictify-hybrid/src/resolution.rs | 16 +- contracts/predictify-hybrid/src/tokens.rs | 144 +++++++++++++++++- 2 files changed, 157 insertions(+), 3 deletions(-) diff --git a/contracts/predictify-hybrid/src/resolution.rs b/contracts/predictify-hybrid/src/resolution.rs index b0b3ce0..51a7587 100644 --- a/contracts/predictify-hybrid/src/resolution.rs +++ b/contracts/predictify-hybrid/src/resolution.rs @@ -1700,8 +1700,20 @@ impl MarketResolutionValidator { .get(&Symbol::new(env, "global_min_pool")) .unwrap_or(0); let min_pool = market.min_pool_size.unwrap_or(global_min); - if min_pool > 0 && market.total_staked < min_pool { - return Err(Error::InvalidState); + + // Only check if min pool is set + if min_pool > 0 { + // Get token decimals to normalize amounts for comparison + let token_client = crate::markets::MarketUtils::get_token_client(env)?; + let token_decimals = token_client.decimals() as u32; + + // Normalize both total staked and min pool to canonical scale for comparison + let normalized_total = crate::tokens::normalize_amount(market.total_staked, token_decimals); + let normalized_min = crate::tokens::normalize_amount(min_pool, token_decimals); + + if normalized_total < normalized_min { + return Err(Error::InvalidState); + } } Ok(()) diff --git a/contracts/predictify-hybrid/src/tokens.rs b/contracts/predictify-hybrid/src/tokens.rs index 6d181dc..ef4e08b 100644 --- a/contracts/predictify-hybrid/src/tokens.rs +++ b/contracts/predictify-hybrid/src/tokens.rs @@ -1,11 +1,67 @@ //! Token management module for Predictify //! Handles multi-asset support for bets and payouts using Soroban token interface. -//! Allows admin to configure allowed tokens per event or globally. +//! Allows admin to configure allowed assets per event or globally. +//! +//! Canonical internal scale: 7 decimals (1 token = 10^7 units). +//! All cross-asset comparisons and calculations use this normalized scale. use crate::err::Error; use alloc::{format, string::ToString}; use soroban_sdk::{token, Address, Env, String, Symbol, Vec}; +/// Canonical internal scale (7 decimals) +pub const CANONICAL_DECIMALS: u32 = 7; + +/// Normalizes an amount from a token's decimal scale to the canonical 7-decimal scale. +/// +/// # Parameters +/// * `amount` - The amount in the token's native decimals +/// * `decimals` - The token's number of decimals +/// +/// # Returns +/// The normalized amount in 7-decimal scale +pub fn normalize_amount(amount: i128, decimals: u32) -> i128 { + if decimals == CANONICAL_DECIMALS { + return amount; + } + + let diff = (decimals as i32 - CANONICAL_DECIMALS as i32).abs(); + let factor = 10i128.pow(diff as u32); + + if decimals > CANONICAL_DECIMALS { + // Need to divide (round down) + amount / factor + } else { + // Need to multiply + amount * factor + } +} + +/// Denormalizes an amount from the canonical 7-decimal scale back to a token's decimal scale. +/// +/// # Parameters +/// * `amount` - The normalized amount in 7-decimal scale +/// * `decimals` - The token's number of decimals +/// +/// # Returns +/// The denormalized amount in the token's native decimals +pub fn denormalize_amount(amount: i128, decimals: u32) -> i128 { + if decimals == CANONICAL_DECIMALS { + return amount; + } + + let diff = (decimals as i32 - CANONICAL_DECIMALS as i32).abs(); + let factor = 10i128.pow(diff as u32); + + if decimals > CANONICAL_DECIMALS { + // Need to multiply + amount * factor + } else { + // Need to divide (round down) + amount / factor + } +} + /// Represents a Stellar asset/token (contract address + symbol). #[soroban_sdk::contracttype] #[derive(Clone, Debug, PartialEq, Eq)] @@ -406,3 +462,89 @@ pub fn validate_token_operation( check_token_balance(env, asset, user, amount)?; Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_normalize_6_decimals() { + // Test a token with 6 decimals (e.g., USDC) + let amount = 1_000_000; // 1 token in 6 decimals + let normalized = normalize_amount(amount, 6); + assert_eq!(normalized, 10_000_000); // Should be 1 token in 7 decimals + } + + #[test] + fn test_normalize_7_decimals() { + // Test native XLM (7 decimals) + let amount = 10_000_000; // 1 XLM + let normalized = normalize_amount(amount, 7); + assert_eq!(normalized, 10_000_000); // Should stay the same + } + + #[test] + fn test_normalize_8_decimals() { + // Test BTC (8 decimals) + let amount = 100_000_000; // 1 BTC + let normalized = normalize_amount(amount, 8); + assert_eq!(normalized, 10_000_000); // 1 token in 7 decimals + } + + #[test] + fn test_normalize_18_decimals() { + // Test ETH (18 decimals) + let amount = 1_000_000_000_000_000_000; // 1 ETH + let normalized = normalize_amount(amount, 18); + assert_eq!(normalized, 10_000_000); // 1 token in 7 decimals + } + + #[test] + fn test_denormalize_6_decimals() { + let normalized = 10_000_000; // 1 token in 7 decimals + let denormalized = denormalize_amount(normalized, 6); + assert_eq!(denormalized, 1_000_000); // 1 token in 6 decimals + } + + #[test] + fn test_denormalize_7_decimals() { + let normalized = 10_000_000; + let denormalized = denormalize_amount(normalized, 7); + assert_eq!(denormalized, 10_000_000); + } + + #[test] + fn test_denormalize_8_decimals() { + let normalized = 10_000_000; + let denormalized = denormalize_amount(normalized, 8); + assert_eq!(denormalized, 100_000_000); + } + + #[test] + fn test_denormalize_18_decimals() { + let normalized = 10_000_000; + let denormalized = denormalize_amount(normalized, 18); + assert_eq!(denormalized, 1_000_000_000_000_000_000); + } + + #[test] + fn test_round_trip_normalize_denormalize() { + // Test 6 decimals + let original_6 = 123_456; + let normalized_6 = normalize_amount(original_6, 6); + let denormalized_6 = denormalize_amount(normalized_6, 6); + assert_eq!(denormalized_6, original_6 / 1); // Since we divide then multiply + + // Test 7 decimals + let original_7 = 12_345_678; + let normalized_7 = normalize_amount(original_7, 7); + let denormalized_7 = denormalize_amount(normalized_7, 7); + assert_eq!(denormalized_7, original_7); + + // Test 8 decimals + let original_8 = 123_456_789; + let normalized_8 = normalize_amount(original_8, 8); + let denormalized_8 = denormalize_amount(normalized_8, 8); + assert_eq!(denormalized_8, (original_8 / 10) * 10); // Precision loss when normalizing down + } +}