Skip to content

Division by Zero in Cross-Price Calculation Under Extreme Value Combinations #2

@caocongyu

Description

@caocongyu

Admin will cause denial of service for cross-price queries affecting users as they write extreme price values triggering division by zero panic.

Summary

The missing divisor validation after precision adjustment in fixed_div_floor will cause a denial of service for users querying cross-prices as the Admin (either maliciously or by mistake) will write extreme price value combinations that trigger division by zero when divisor < 10^bshift.

Root Cause

In oracle/src/prices.rs:328-344, the precision adjustment logic divides the divisor by 10^bshift without checking if the result becomes zero:

pub fn fixed_div_floor(dividend: i128, divisor: i128, decimals: u32) -> i128 {
    if dividend <= 0 || divisor <= 0 {
        panic!("invalid division arguments")
    }
    let ashift = core::cmp::min(38 - dividend.ilog10(), decimals);
    let bshift = core::cmp::max(decimals - ashift, 0);

    let mut vdividend = dividend;
    let mut vdivisor = divisor;
    if ashift > 0 {
        vdividend *= 10_i128.pow(ashift);
    }
    if bshift > 0 {
        vdivisor /= 10_i128.pow(bshift);  // ⚠️ Can become 0 when divisor < 10^bshift
    }
    vdividend / vdivisor  // ⚠️ Division by zero panic
}

When the dividend is extremely large (e.g., 1.7×10^35), ashift becomes small (e.g., 3) and bshift becomes large (e.g., 11). If the divisor is smaller than 10^bshift (e.g., divisor=2 < 10^11), then vdivisor becomes 0, triggering division by zero.

Internal Pre-conditions

  1. Admin needs to call set_price() to set one asset price to be at least 10^35 (approximately $10^21 USD equivalent with 14 decimals)
  2. Admin needs to call set_price() to set another asset price to be at most 10^11 (approximately $0.0001 USD equivalent with 14 decimals)

External Pre-conditions

None.

Attack Path

  1. Admin calls set_price(base_asset, 1.7×10^35, timestamp) to write an extremely large price for the base asset
  2. Admin calls set_price(quote_asset, 2, timestamp) to write an extremely small price for the quote asset
  3. User calls x_last_price(base_asset, quote_asset) to query the cross-price
  4. The contract calls load_cross_price() which retrieves both prices
  5. fixed_div_floor(1.7×10^35, 2, 14) is invoked to calculate base_price / quote_price
  6. During precision adjustment: ashift=3, bshift=11, causing vdivisor = 2 / 10^11 = 0
  7. The division vdividend / vdivisor panics with "attempt to divide by zero"
  8. The transaction reverts, denying service to all users querying this cross-price pair

Impact

Users cannot query cross-prices for affected asset pairs. All four cross-price query functions (x_last_price, x_price, x_prices, x_twap) will panic and revert transactions. However, no funds are lost as the panic causes a revert without state changes.

Severity Mitigation Factors:

  • Requires Admin privileges (multisig with >50% node consensus)
  • Requires unrealistic price values (real-world prices range from $10^10 to $10^19 with 14 decimals, far below the $10^35 trigger threshold)
  • Falls under "Administrator Mistakes" publicly known issues acknowledged in the project README
  • Attack cost (compromising multisig) vastly exceeds benefit ($0 due to revert)

PoC

Add this test to oracle/src/tests/util_tests.rs:

#[test]
#[should_panic(expected = "attempt to divide by zero")]
fn test_fixed_div_extreme_values_poc() {
    // Simulates Admin writing extreme price combinations
    let large_dividend = i128::MAX / 1000;  // ≈ 1.7×10^35 (extremely large price)
    let small_divisor = 2;                   // Very small price
    let decimals = 14u32;                    // Standard precision
    
    // This will panic with division by zero
    // Calculation: ashift=3, bshift=11
    // vdivisor = 2 / 10^11 = 0 (integer division)
    // vdividend / 0 → panic
    let _result = prices::fixed_div_floor(large_dividend, small_divisor, decimals);
}

#[test]
fn test_realistic_prices_are_safe() {
    // Real-world price ranges (with 14 decimals)
    let btc_price = 100_000 * 10_i128.pow(14);  // $100K BTC = 10^19
    let min_price = 1 * 10_i128.pow(10);         // $0.0001 min = 10^10
    
    // All realistic combinations work fine
    let result = prices::fixed_div_floor(btc_price, min_price, 14);
    assert!(result > 0);
    
    // Extreme values needed to trigger bug are 10^16 times larger
    // Unrealistic in any real-world scenario
}

Run with: cargo test test_fixed_div_extreme_values_poc

Add defensive validation after precision adjustment:

pub fn fixed_div_floor(dividend: i128, divisor: i128, decimals: u32) -> i128 {
    if dividend <= 0 || divisor <= 0 {
        panic!("invalid division arguments")
    }
    let ashift = core::cmp::min(38 - dividend.ilog10(), decimals);
    let bshift = core::cmp::max(decimals - ashift, 0);

    let mut vdividend = dividend;
    let mut vdivisor = divisor;
    if ashift > 0 {
        vdividend *= 10_i128.pow(ashift);
    }
    if bshift > 0 {
        let shift_divisor = 10_i128.pow(bshift);
        vdivisor /= shift_divisor;
        
+       if vdivisor == 0 {
+           panic!("divisor too small for precision adjustment");
+       }
    }
    vdividend / vdivisor
}

Alternative: Add price range validation in set_price() to reject extreme values before they are stored.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions