-
Notifications
You must be signed in to change notification settings - Fork 15
Description
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
- Admin needs to call
set_price()to set one asset price to be at least10^35(approximately $10^21 USD equivalent with 14 decimals) - Admin needs to call
set_price()to set another asset price to be at most10^11(approximately $0.0001 USD equivalent with 14 decimals)
External Pre-conditions
None.
Attack Path
- Admin calls
set_price(base_asset, 1.7×10^35, timestamp)to write an extremely large price for the base asset - Admin calls
set_price(quote_asset, 2, timestamp)to write an extremely small price for the quote asset - User calls
x_last_price(base_asset, quote_asset)to query the cross-price - The contract calls
load_cross_price()which retrieves both prices fixed_div_floor(1.7×10^35, 2, 14)is invoked to calculatebase_price / quote_price- During precision adjustment:
ashift=3, bshift=11, causingvdivisor = 2 / 10^11 = 0 - The division
vdividend / vdivisorpanics with "attempt to divide by zero" - 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.