Skip to content

Commit 91537d0

Browse files
committed
feat: weighted median oracle aggregation
1 parent 43fdfdd commit 91537d0

1 file changed

Lines changed: 136 additions & 84 deletions

File tree

contracts/predictify-hybrid/src/oracles.rs

Lines changed: 136 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -2448,6 +2448,8 @@ pub enum OracleIntegrationKey {
24482448
VerificationStatus(Symbol),
24492449
/// Retry count for market verification
24502450
RetryCount(Symbol),
2451+
/// Configurable per-source weight
2452+
OracleWeight(Address),
24512453
}
24522454

24532455
/// Storage keys for oracle validation configuration.
@@ -2831,6 +2833,61 @@ impl OracleValidationConfigManager {
28312833
pub struct OracleIntegrationManager;
28322834

28332835
impl OracleIntegrationManager {
2836+
/// Set configurable weight for a specific oracle source
2837+
pub fn set_oracle_weight(
2838+
env: &Env,
2839+
admin: Address,
2840+
oracle: Address,
2841+
weight: u32,
2842+
) -> Result<(), Error> {
2843+
OracleWhitelist::require_admin(env, &admin)?;
2844+
env.storage()
2845+
.persistent()
2846+
.set(&OracleIntegrationKey::OracleWeight(oracle), &weight);
2847+
Ok(())
2848+
}
2849+
2850+
/// Get configured weight for an oracle source, defaults to 1
2851+
pub fn get_oracle_weight(env: &Env, oracle: &Address) -> u32 {
2852+
env.storage()
2853+
.persistent()
2854+
.get(&OracleIntegrationKey::OracleWeight(oracle.clone()))
2855+
.unwrap_or(1)
2856+
}
2857+
2858+
/// Calculate the weighted median price safely
2859+
fn calculate_weighted_median(
2860+
_env: &Env,
2861+
readings: &alloc::vec::Vec<(i128, u32)>,
2862+
total_weight: u32,
2863+
) -> i128 {
2864+
if readings.is_empty() {
2865+
return 0;
2866+
}
2867+
2868+
let mut sorted: alloc::vec::Vec<(i128, u32)> =
2869+
alloc::vec::Vec::with_capacity(readings.len());
2870+
for r in readings.iter() {
2871+
sorted.push(r.clone());
2872+
}
2873+
sorted.sort_unstable_by(|a, b| a.0.cmp(&b.0));
2874+
2875+
let target = total_weight / 2;
2876+
let mut accumulated: u32 = 0;
2877+
2878+
for (price, weight) in sorted.iter() {
2879+
accumulated = accumulated.saturating_add(*weight);
2880+
if accumulated > target {
2881+
return *price;
2882+
}
2883+
}
2884+
2885+
if let Some(last) = sorted.last() {
2886+
last.0
2887+
} else {
2888+
0
2889+
}
2890+
}
28342891
/// Legacy defaults (actual validation uses OracleValidationConfigManager)
28352892
const MAX_DATA_AGE_SECONDS: u64 = 60;
28362893
/// Minimum confidence score required (not currently enforced here)
@@ -2938,8 +2995,8 @@ impl OracleIntegrationManager {
29382995
use crate::events::EventEmitter;
29392996

29402997
let oracle_config = &market.oracle_config;
2941-
let mut successful_results: Vec<(i128, String)> = Vec::new(env);
2942-
let mut total_price: i128 = 0;
2998+
let mut successful_readings: alloc::vec::Vec<(i128, u32)> = alloc::vec::Vec::new();
2999+
let mut total_weight: u32 = 0;
29433000
let mut sources_count: u32 = 0;
29443001
let mut last_error: Option<Error> = None;
29453002

@@ -2955,17 +3012,12 @@ impl OracleIntegrationManager {
29553012
Ok(price) => {
29563013
// Validate price is within acceptable range
29573014
if Self::validate_price_range(price) {
2958-
// Determine outcome for this source
2959-
let outcome = OracleUtils::determine_outcome(
2960-
price,
2961-
oracle_config.threshold,
2962-
&oracle_config.comparison,
2963-
env,
2964-
)?;
2965-
2966-
successful_results.push_back((price, outcome));
2967-
total_price += price;
2968-
sources_count += 1;
3015+
let weight = Self::get_oracle_weight(env, &oracle_address);
3016+
if weight > 0 {
3017+
successful_readings.push((price, weight));
3018+
total_weight = total_weight.saturating_add(weight);
3019+
sources_count += 1;
3020+
}
29693021
}
29703022
}
29713023
Err(e) => {
@@ -2988,29 +3040,55 @@ impl OracleIntegrationManager {
29883040
return Err(Error::OracleUnavailable);
29893041
}
29903042

2991-
// Calculate average price
2992-
let average_price = total_price / (sources_count as i128);
3043+
// Calculate weighted median price
3044+
let median_price = Self::calculate_weighted_median(env, &successful_readings, total_weight);
29933045

2994-
// Calculate price variance (simplified - max deviation from average)
3046+
// Determine final outcome directly from the weighted median price
3047+
let final_outcome = OracleUtils::determine_outcome(
3048+
median_price,
3049+
oracle_config.threshold,
3050+
&oracle_config.comparison,
3051+
env,
3052+
)?;
3053+
3054+
// Calculate agreement count for confidence score and legacy events
3055+
let mut agreement_count: u32 = 0;
3056+
let mut agreement_weight: u32 = 0;
3057+
for (price, weight) in successful_readings.iter() {
3058+
let outcome = OracleUtils::determine_outcome(
3059+
*price,
3060+
oracle_config.threshold,
3061+
&oracle_config.comparison,
3062+
env,
3063+
)?;
3064+
if outcome == final_outcome {
3065+
agreement_count += 1;
3066+
agreement_weight = agreement_weight.saturating_add(*weight);
3067+
}
3068+
}
3069+
3070+
// Calculate price variance (simplified - max deviation from median)
29953071
let mut max_deviation: i128 = 0;
2996-
for (price, _) in successful_results.iter() {
2997-
let deviation = if price > average_price {
2998-
price - average_price
3072+
for (price, _) in successful_readings.iter() {
3073+
let deviation = if *price > median_price {
3074+
*price - median_price
29993075
} else {
3000-
average_price - price
3076+
median_price - *price
30013077
};
30023078
if deviation > max_deviation {
30033079
max_deviation = deviation;
30043080
}
30053081
}
30063082

3007-
// Determine consensus outcome
3008-
let (final_outcome, consensus_reached, agreement_count) =
3009-
Self::determine_consensus_outcome(env, &successful_results)?;
3083+
let agreement_percentage = if total_weight > 0 {
3084+
(agreement_weight * 100) / total_weight
3085+
} else {
3086+
0
3087+
};
30103088

3011-
let agreement_percentage = (agreement_count * 100) / sources_count;
3089+
// Check consensus threshold based on weight agreement
3090+
let consensus_reached = agreement_percentage >= Self::DEFAULT_CONSENSUS_THRESHOLD;
30123091

3013-
// Check consensus threshold
30143092
if !consensus_reached {
30153093
EventEmitter::emit_oracle_verification_failed(
30163094
env,
@@ -3030,23 +3108,23 @@ impl OracleIntegrationManager {
30303108
&final_outcome,
30313109
agreement_count,
30323110
sources_count,
3033-
average_price,
3111+
median_price,
30343112
max_deviation,
30353113
);
30363114

30373115
// Calculate confidence score based on agreement and price stability
30383116
let confidence_score = Self::calculate_confidence_score(
30393117
agreement_percentage,
30403118
max_deviation,
3041-
average_price,
3119+
median_price,
30423120
sources_count,
30433121
);
30443122

30453123
// Build the oracle result
30463124
Ok(crate::types::OracleResult {
30473125
market_id: market_id.clone(),
30483126
outcome: final_outcome,
3049-
price: average_price,
3127+
price: median_price,
30503128
threshold: oracle_config.threshold,
30513129
comparison: oracle_config.comparison.clone(),
30523130
provider: oracle_config.provider.clone(),
@@ -3101,39 +3179,7 @@ impl OracleIntegrationManager {
31013179
Ok(price_data.price)
31023180
}
31033181

3104-
/// Determine consensus outcome from multiple oracle results.
3105-
fn determine_consensus_outcome(
3106-
env: &Env,
3107-
results: &Vec<(i128, String)>,
3108-
) -> Result<(String, bool, u32), Error> {
3109-
if results.is_empty() {
3110-
return Err(Error::OracleUnavailable);
3111-
}
3112-
3113-
// Count outcomes
3114-
let mut yes_count: u32 = 0;
3115-
let mut no_count: u32 = 0;
3116-
3117-
for (_, outcome) in results.iter() {
3118-
if outcome == String::from_str(env, "yes") {
3119-
yes_count += 1;
3120-
} else {
3121-
no_count += 1;
3122-
}
3123-
}
3124-
3125-
let total = results.len() as u32;
3126-
let (final_outcome, agreement_count) = if yes_count >= no_count {
3127-
(String::from_str(env, "yes"), yes_count)
3128-
} else {
3129-
(String::from_str(env, "no"), no_count)
3130-
};
3131-
3132-
let agreement_percentage = (agreement_count * 100) / total;
3133-
let consensus_reached = agreement_percentage >= Self::DEFAULT_CONSENSUS_THRESHOLD;
31343182

3135-
Ok((final_outcome, consensus_reached, agreement_count))
3136-
}
31373183

31383184
/// Calculate confidence score based on multiple factors.
31393185
fn calculate_confidence_score(
@@ -3398,32 +3444,38 @@ mod oracle_integration_tests {
33983444
}
33993445

34003446
#[test]
3401-
fn test_determine_consensus_outcome() {
3447+
fn test_calculate_weighted_median() {
34023448
let env = Env::default();
34033449

3404-
// All agree on "yes"
3405-
let mut results: Vec<(i128, String)> = Vec::new(&env);
3406-
results.push_back((50_000_00, String::from_str(&env, "yes")));
3407-
results.push_back((50_100_00, String::from_str(&env, "yes")));
3408-
results.push_back((49_900_00, String::from_str(&env, "yes")));
3409-
3410-
let (outcome, consensus, count) =
3411-
OracleIntegrationManager::determine_consensus_outcome(&env, &results).unwrap();
3412-
assert_eq!(outcome, String::from_str(&env, "yes"));
3413-
assert!(consensus);
3414-
assert_eq!(count, 3);
3415-
3416-
// Mixed results - 2 yes, 1 no (67% agreement)
3417-
let mut mixed_results: Vec<(i128, String)> = Vec::new(&env);
3418-
mixed_results.push_back((50_000_00, String::from_str(&env, "yes")));
3419-
mixed_results.push_back((50_100_00, String::from_str(&env, "yes")));
3420-
mixed_results.push_back((49_000_00, String::from_str(&env, "no")));
3421-
3422-
let (outcome, consensus, count) =
3423-
OracleIntegrationManager::determine_consensus_outcome(&env, &mixed_results).unwrap();
3424-
assert_eq!(outcome, String::from_str(&env, "yes"));
3425-
assert!(consensus); // 67% meets 66% threshold
3426-
assert_eq!(count, 2);
3450+
// 1. Single reading
3451+
let mut readings_single: alloc::vec::Vec<(i128, u32)> = alloc::vec::Vec::new();
3452+
readings_single.push((100, 5));
3453+
assert_eq!(OracleIntegrationManager::calculate_weighted_median(&env, &readings_single, 5), 100);
3454+
3455+
// 2. Even total weight, typical scenario
3456+
// weights: 10, 20, 30. Total weight = 60. Target = 30.
3457+
// prices: 100, 200, 300
3458+
// accumulated weights: 10 (at 100), 30 (at 200), 60 (at 300)
3459+
// target = 30. The first where accumulated > 30 is the last one (60).
3460+
let mut readings_even: alloc::vec::Vec<(i128, u32)> = alloc::vec::Vec::new();
3461+
readings_even.push((200, 20));
3462+
readings_even.push((100, 10));
3463+
readings_even.push((300, 30));
3464+
assert_eq!(OracleIntegrationManager::calculate_weighted_median(&env, &readings_even, 60), 300);
3465+
3466+
// 3. Odd total weight
3467+
// weights: 10, 10, 10. Total = 30. Target = 15.
3468+
// prices: 100, 200, 300
3469+
// accumulated: 10, 20 (takes 200)
3470+
let mut readings_odd: alloc::vec::Vec<(i128, u32)> = alloc::vec::Vec::new();
3471+
readings_odd.push((300, 10));
3472+
readings_odd.push((100, 10));
3473+
readings_odd.push((200, 10));
3474+
assert_eq!(OracleIntegrationManager::calculate_weighted_median(&env, &readings_odd, 30), 200);
3475+
3476+
// 4. Empty readings
3477+
let readings_empty: alloc::vec::Vec<(i128, u32)> = alloc::vec::Vec::new();
3478+
assert_eq!(OracleIntegrationManager::calculate_weighted_median(&env, &readings_empty, 0), 0);
34273479
}
34283480

34293481
#[test]

0 commit comments

Comments
 (0)