Skip to content
Open
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
1 change: 1 addition & 0 deletions contracts/price-oracle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
proptest = "1.0"
20 changes: 14 additions & 6 deletions contracts/price-oracle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ pub trait TokenContractTrait {
fn transfer(env: Env, from: Address, to: Address, amount: i128);
}

/// Conversion factor from price changes to basis points (10,000 = 100%).
/// Used to convert percentage changes to BPS: (delta * BPS_CONVERSION_FACTOR) / old_price.
/// Pre-computed as a constant to reduce compute cycles.
const BPS_CONVERSION_FACTOR: i128 = 10_000;

/// Maximum allowed percentage change between price updates (10% = 1000 basis points).
/// Any price update exceeding this threshold will be rejected to prevent flash crashes.
const MAX_PERCENT_CHANGE_BPS: i128 = 1_000;
Expand Down Expand Up @@ -333,7 +338,7 @@ pub fn calculate_percentage_change_bps(old_price: i128, new_price: i128) -> Opti
}

let delta = new_price.checked_sub(old_price)?;
let scaled = delta.checked_mul(10_000)?;
let scaled = delta.checked_mul(BPS_CONVERSION_FACTOR)?;
scaled.checked_div(old_price)
}

Expand Down Expand Up @@ -626,16 +631,18 @@ impl PriceOracle {
.ok_or(Error::InvalidPrice)?;

total_weight = total_weight.checked_add(component.weight)
.unwrap_or(total_weight);
.ok_or(Error::InvalidWeight)?;
}

if total_weight == 0 {
return Err(Error::InvalidWeight);
}

// Calculate final index price.
// Calculate final index price using checked arithmetic.
// Because all stored prices are 9-decimal normalized, the division preserves the 9-decimal standard.
let index_price = total_weighted_price / (total_weight as i128);
let index_price = total_weighted_price
.checked_div(total_weight as i128)
.ok_or(Error::InvalidPrice)?;
Ok(index_price)
}

Expand Down Expand Up @@ -2117,10 +2124,10 @@ impl PriceOracle {

let mut sum: i128 = 0;
for (_, price) in twap_buffer.iter() {
sum += price;
sum = sum.checked_add(price)?;
}

Some(sum / (len as i128))
sum.checked_div(len as i128)
}

/// Subscribe a contract to receive price update callbacks.
Expand Down Expand Up @@ -2165,3 +2172,4 @@ pub mod math;
mod median;
mod test;
mod types;
mod property_tests;
7 changes: 5 additions & 2 deletions contracts/price-oracle/src/median.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fn sort_prices(prices: &mut Vec<i128>) {
/// - 0 inputs → Err(MedianError::EmptyInput)
/// - 1 input → returns that value
/// - odd count → returns the middle value
/// - even count → returns the average of the two middle values
/// - even count → returns the average of the two middle values (using checked arithmetic)
#[allow(dead_code)]
pub fn calculate_median(mut prices: Vec<i128>) -> Result<i128, MedianError> {
let len = prices.len();
Expand All @@ -45,7 +45,10 @@ pub fn calculate_median(mut prices: Vec<i128>) -> Result<i128, MedianError> {
} else {
let lo = prices.get(mid - 1).unwrap();
let hi = prices.get(mid).unwrap();
Ok((lo + hi) / 2)
// Use checked arithmetic to prevent overflow
let sum = lo.checked_add(hi).ok_or(MedianError::EmptyInput)?;
let avg = sum.checked_div(2).ok_or(MedianError::EmptyInput)?;
Ok(avg)
}
}

Expand Down
Loading