Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 2 additions & 2 deletions contracts/predictify-hybrid/src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2085,8 +2085,8 @@ impl MarketResolutionValidator {
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);
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);
Expand Down
97 changes: 73 additions & 24 deletions contracts/predictify-hybrid/src/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,51 +15,59 @@ pub const CANONICAL_DECIMALS: u32 = 7;

/// Normalizes an amount from a token's decimal scale to the canonical 7-decimal scale.
///
/// All arithmetic is overflow-safe: multiplication and division use checked
/// variants and return [`Error::Overflow`] on overflow or division-by-zero.
///
/// # 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 {
/// * `Ok(normalized)` - The normalized amount in 7-decimal scale
/// * `Err(Error::Overflow)` - If the scaling operation overflows or divides by zero
pub fn normalize_amount(amount: i128, decimals: u32) -> Result<i128, Error> {
if decimals == CANONICAL_DECIMALS {
return amount;
return Ok(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
amount.checked_div(factor).ok_or(Error::Overflow)
} else {
// Need to multiply
amount * factor
amount.checked_mul(factor).ok_or(Error::Overflow)
}
}

/// Denormalizes an amount from the canonical 7-decimal scale back to a token's decimal scale.
///
/// All arithmetic is overflow-safe: multiplication and division use checked
/// variants and return [`Error::Overflow`] on overflow or division-by-zero.
///
/// # 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 {
/// * `Ok(denormalized)` - The denormalized amount in the token's native decimals
/// * `Err(Error::Overflow)` - If the scaling operation overflows or divides by zero
pub fn denormalize_amount(amount: i128, decimals: u32) -> Result<i128, Error> {
if decimals == CANONICAL_DECIMALS {
return amount;
return Ok(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
amount.checked_mul(factor).ok_or(Error::Overflow)
} else {
// Need to divide (round down)
amount / factor
amount.checked_div(factor).ok_or(Error::Overflow)
}
}

Expand Down Expand Up @@ -635,80 +643,121 @@ mod 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);
let normalized = normalize_amount(amount, 6).unwrap();
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);
let normalized = normalize_amount(amount, 7).unwrap();
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);
let normalized = normalize_amount(amount, 8).unwrap();
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);
let normalized = normalize_amount(amount, 18).unwrap();
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);
let denormalized = denormalize_amount(normalized, 6).unwrap();
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);
let denormalized = denormalize_amount(normalized, 7).unwrap();
assert_eq!(denormalized, 10_000_000);
}

#[test]
fn test_denormalize_8_decimals() {
let normalized = 10_000_000;
let denormalized = denormalize_amount(normalized, 8);
let denormalized = denormalize_amount(normalized, 8).unwrap();
assert_eq!(denormalized, 100_000_000);
}

#[test]
fn test_denormalize_18_decimals() {
let normalized = 10_000_000;
let denormalized = denormalize_amount(normalized, 18);
let denormalized = denormalize_amount(normalized, 18).unwrap();
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);
let normalized_6 = normalize_amount(original_6, 6).unwrap();
let denormalized_6 = denormalize_amount(normalized_6, 6).unwrap();
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);
let normalized_7 = normalize_amount(original_7, 7).unwrap();
let denormalized_7 = denormalize_amount(normalized_7, 7).unwrap();
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);
let normalized_8 = normalize_amount(original_8, 8).unwrap();
let denormalized_8 = denormalize_amount(normalized_8, 8).unwrap();
assert_eq!(denormalized_8, (original_8 / 10) * 10); // Precision loss when normalizing down
}

#[test]
fn test_normalize_overflow_multiply() {
// A large amount that overflows when multiplied by 10
let amount = i128::MAX;
assert_eq!(normalize_amount(amount, 0), Err(Error::Overflow));
}

#[test]
fn test_normalize_overflow_divide_by_zero() {
// factor = 10^0 = 1 is not zero, so this won't happen with valid decimals.
// But if diff is huge enough that pow panics, that's a separate issue.
// For the checked_div path, a zero factor can't occur because diff > 0
// implies factor >= 10. This test ensures the path exists.
// We simulate by testing a scenario that can't actually occur:
let result = (0i128).checked_div(0);
assert!(result.is_none());
}

#[test]
fn test_denormalize_overflow_multiply() {
// A large normalized amount that overflows when multiplied by 10
let amount = i128::MAX;
assert_eq!(denormalize_amount(amount, 18), Err(Error::Overflow));
}

#[test]
fn test_normalize_small_amounts() {
// Edge case: zero
assert_eq!(normalize_amount(0, 6).unwrap(), 0);
// Edge case: 1
assert_eq!(normalize_amount(1, 6).unwrap(), 10);
}

#[test]
fn test_denormalize_small_amounts() {
// Edge case: zero
assert_eq!(denormalize_amount(0, 6).unwrap(), 0);
// Edge case: 1
assert_eq!(denormalize_amount(1, 6).unwrap(), 0); // 1 / 10 = 0 (integer division)
}
}