Skip to content

Commit 775e022

Browse files
authored
feat: integrate external price feeds (Pyth Network) for automated mar… (#374)
* feat: integrate external price feeds (Pyth Network) for automated market resolution - Add Pyth Network SDK dependency to workspace and contract - Implement price feed adapter with oracle configuration management - Add price condition system for automated resolution rules - Integrate price-based pool resolution with timestamp validation - Add comprehensive role-based access control for oracle operations - Implement batch price feed updates and cleanup functionality - Add detailed events for oracle operations and price resolution - Create comprehensive test suite for price feed functionality - Ensure proper code formatting and all tests passing Features: - Oracle initialization with configurable parameters - Price feed updates with validation and freshness checks - Price condition setting (equal, greater than, less than operators) - Automated market resolution based on price conditions - Support for tolerance-based price matching - Batch operations for efficiency - Comprehensive error handling and validation * fix: resolve clippy warning for manual Option::map implementation - Simplified get_oracle_config method to use direct return instead of manual Option mapping - Resolves clippy::manual_map warning - Maintains same functionality while improving code quality
1 parent 50eaeef commit 775e022

File tree

7 files changed

+1099
-7
lines changed

7 files changed

+1099
-7
lines changed

contract/Cargo.lock

Lines changed: 121 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contract/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ members = [
1111
soroban-sdk = "23"
1212
predifi-errors = { path = "contracts/predifi-errors" }
1313
access-control = { path = "contracts/access-control" }
14+
# Pyth Network dependencies for price feeds
15+
pyth-sdk = "0.3.0"
1416

1517
# ── Release profile (optimised for WASM size) ──────────────────────────────────
1618
[profile.release]

contract/contracts/predifi-contract/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ crate-type = ["cdylib", "lib"]
1212
soroban-sdk = { workspace = true }
1313
predifi-errors = { workspace = true }
1414
access-control = { workspace = true }
15+
# Pyth Network for price feeds
16+
pyth-sdk = { workspace = true }
1517

1618
[dev-dependencies]
1719
soroban-sdk = { workspace = true, features = ["testutils"] }

contract/contracts/predifi-contract/src/lib.rs

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![no_std]
22
#![allow(clippy::too_many_arguments)]
33

4+
mod price_feed_simple;
45
mod safe_math;
56
#[cfg(test)]
67
mod safe_math_examples;
@@ -14,6 +15,7 @@ use soroban_sdk::{
1415
Env, IntoVal, String, Symbol, Vec,
1516
};
1617

18+
pub use price_feed_simple::PriceFeedAdapter;
1719
pub use safe_math::{RoundingMode, SafeMath};
1820

1921
// ═══════════════════════════════════════════════════════════════════════════
@@ -61,6 +63,14 @@ pub enum PredifiError {
6163
InvalidAmount = 42,
6264
/// Insufficient balance for the operation.
6365
InsufficientBalance = 44,
66+
/// Oracle not initialized.
67+
OracleNotInitialized = 100,
68+
/// Price feed not found.
69+
PriceFeedNotFound = 101,
70+
/// Price data expired or invalid.
71+
PriceDataInvalid = 102,
72+
/// Price condition not set for pool.
73+
PriceConditionNotSet = 103,
6474
}
6575

6676
#[contracttype]
@@ -414,6 +424,46 @@ pub struct UpgradeEvent {
414424
pub new_wasm_hash: BytesN<32>,
415425
}
416426

427+
#[contractevent(topics = ["oracle_init"])]
428+
#[derive(Clone, Debug, Eq, PartialEq)]
429+
pub struct OracleInitEvent {
430+
pub admin: Address,
431+
pub pyth_contract: Address,
432+
pub max_price_age: u64,
433+
pub min_confidence_ratio: u32,
434+
}
435+
436+
#[contractevent(topics = ["price_feed_updated"])]
437+
#[derive(Clone, Debug, Eq, PartialEq)]
438+
pub struct PriceFeedUpdatedEvent {
439+
pub oracle: Address,
440+
pub feed_pair: Symbol,
441+
pub price: i128,
442+
pub confidence: i128,
443+
pub timestamp: u64,
444+
pub expires_at: u64,
445+
}
446+
447+
#[contractevent(topics = ["price_condition_set"])]
448+
#[derive(Clone, Debug, Eq, PartialEq)]
449+
pub struct PriceConditionSetEvent {
450+
pub pool_id: u64,
451+
pub feed_pair: Symbol,
452+
pub target_price: i128,
453+
pub operator: u32,
454+
pub tolerance_bps: u32,
455+
}
456+
457+
#[contractevent(topics = ["price_resolved"])]
458+
#[derive(Clone, Debug, Eq, PartialEq)]
459+
pub struct PriceResolvedEvent {
460+
pub pool_id: u64,
461+
pub feed_pair: Symbol,
462+
pub current_price: i128,
463+
pub target_price: i128,
464+
pub outcome: u32,
465+
}
466+
417467
// ─────────────────────────────────────────────────────────────────────────────
418468

419469
pub trait OracleCallback {
@@ -1680,11 +1730,11 @@ impl OracleCallback for PredifiContract {
16801730
outcome: u32,
16811731
proof: String,
16821732
) -> Result<(), PredifiError> {
1683-
PredifiContract::require_not_paused(&env);
1733+
Self::require_not_paused(&env);
16841734
oracle.require_auth();
16851735

16861736
// Check authorization: oracle must have role 3
1687-
if let Err(e) = PredifiContract::require_role(&env, &oracle, 3) {
1737+
if let Err(e) = Self::require_role(&env, &oracle, 3) {
16881738
// 🔴 HIGH ALERT: unauthorized attempt to resolve a pool by an oracle
16891739
UnauthorizedResolveAttemptEvent {
16901740
caller: oracle,
@@ -1709,7 +1759,7 @@ impl OracleCallback for PredifiContract {
17091759
}
17101760

17111761
let current_time = env.ledger().timestamp();
1712-
let config = PredifiContract::get_config(&env);
1762+
let config = Self::get_config(&env);
17131763

17141764
if current_time < pool.end_time.saturating_add(config.resolution_delay) {
17151765
return Err(PredifiError::ResolutionDelayNotMet);
@@ -1719,7 +1769,7 @@ impl OracleCallback for PredifiContract {
17191769
// Verify state transition validity (INV-2)
17201770
assert!(
17211771
outcome < pool.options_count
1722-
&& PredifiContract::is_valid_state_transition(pool.state, MarketState::Resolved),
1772+
&& Self::is_valid_state_transition(pool.state, MarketState::Resolved),
17231773
"outcome exceeds options_count or invalid state transition"
17241774
);
17251775

@@ -1728,10 +1778,10 @@ impl OracleCallback for PredifiContract {
17281778
pool.outcome = outcome;
17291779

17301780
env.storage().persistent().set(&pool_key, &pool);
1731-
PredifiContract::extend_persistent(&env, &pool_key);
1781+
Self::extend_persistent(&env, &pool_key);
17321782

17331783
// Retrieve winning-outcome stake for the diagnostic event using optimized batch storage
1734-
let stakes = PredifiContract::get_outcome_stakes(&env, pool_id, pool.options_count);
1784+
let stakes = Self::get_outcome_stakes(&env, pool_id, pool.options_count);
17351785
let winning_stake: i128 = stakes.get(outcome).unwrap_or(0);
17361786

17371787
OracleResolvedEvent {

0 commit comments

Comments
 (0)