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
332 changes: 320 additions & 12 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ anchor-lang = "0.31.1"
flaky_test = "0.2.2"
hylo-fix = "0.4.2"
pyth-solana-receiver-sdk = "1.0.1"
switchboard-on-demand = "0.10.8"
base64 = "0.22.1"
serde_json = "1.0.140"
tokio = "1.36.0"
Expand Down
2 changes: 2 additions & 0 deletions hylo-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ hylo-fix.workspace = true
anchor-lang.workspace = true
anchor-spl.workspace = true
pyth-solana-receiver-sdk.workspace = true
switchboard-on-demand = { workspace = true, features = ["anchor"] }
rust_decimal.workspace = true
jupiter-amm-interface = { workspace = true, optional = true }
hylo-idl = { version = "0.1.16", optional = true }
serde.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion hylo-core/src/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use fix::prelude::*;
use crate::error::CoreError::{
LeverToStable, LstToToken, StableToLever, TokenToLst,
};
use crate::pyth::PriceRange;
use crate::oracle::PriceRange;

/// Provides conversions between an LST and protocol tokens.
pub struct Conversion {
Expand Down
11 changes: 11 additions & 0 deletions hylo-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ pub enum CoreError {
PythOracleSlotInvalid,
#[msg("Oracle price update is not fully verified.")]
PythOracleVerificationLevel,
// `switchboard`
#[msg("Switchboard feed is stale and exceeds max staleness.")]
SwitchboardOracleStale,
#[msg("Switchboard feed has insufficient oracle responses.")]
SwitchboardOracleInsufficientSamples,
#[msg("Switchboard feed value is missing or invalid.")]
SwitchboardOracleInvalidValue,
#[msg("Switchboard oracle standard deviation exceeds tolerance.")]
SwitchboardOracleStdDevTooHigh,
#[msg("Switchboard oracle price range calculation overflow.")]
SwitchboardOraclePriceRange,
// `nav`
#[msg("Overflow while computing collateral ratio.")]
CollateralRatio,
Expand Down
10 changes: 4 additions & 6 deletions hylo-core/src/exchange_context.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use anchor_lang::prelude::*;
use anchor_spl::token::Mint;
use fix::prelude::*;
use pyth_solana_receiver_sdk::price_update::PriceUpdateV2;

use crate::conversion::{Conversion, SwapConversion};
use crate::error::CoreError::{
Expand All @@ -17,7 +16,7 @@ use crate::fee_controller::{
FeeController, FeeExtract, LevercoinFees, StablecoinFees,
};
use crate::lst_sol_price::LstSolPrice;
use crate::pyth::{query_pyth_price, OracleConfig, PriceRange};
use crate::oracle::{OracleConfig, OraclePrice, PriceRange};
use crate::solana_clock::SolanaClock;
use crate::stability_mode::{StabilityController, StabilityMode};
use crate::stability_pool_math::stability_pool_cap;
Expand All @@ -40,20 +39,19 @@ pub struct ExchangeContext<C> {
impl<C: SolanaClock> ExchangeContext<C> {
/// Creates main context for exchange operations from account data.
#[allow(clippy::too_many_arguments)]
pub fn load(
pub fn load<O: OraclePrice>(
clock: C,
total_sol_cache: &TotalSolCache,
stability_controller: StabilityController,
oracle_config: OracleConfig<N8>,
stablecoin_fees: StablecoinFees,
levercoin_fees: LevercoinFees,
sol_usd_pyth_feed: &PriceUpdateV2,
sol_usd_oracle: &O,
stablecoin_mint: &Mint,
levercoin_mint: Option<&Mint>,
) -> Result<ExchangeContext<C>> {
let total_sol = total_sol_cache.get_validated(clock.epoch())?;
let sol_usd_price =
query_pyth_price(&clock, sol_usd_pyth_feed, oracle_config)?;
let sol_usd_price = sol_usd_oracle.query_price(&clock, oracle_config)?;
let stablecoin_supply = UFix64::new(stablecoin_mint.supply);
let levercoin_supply = levercoin_mint.map(|m| UFix64::new(m.supply));
let collateral_ratio =
Expand Down
2 changes: 1 addition & 1 deletion hylo-core/src/exchange_math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::error::CoreError::{
CollateralRatio, MaxMintable, MaxSwappable, StablecoinNav,
TargetCollateralRatioTooLow, TotalValueLocked,
};
use crate::pyth::PriceRange;
use crate::oracle::PriceRange;

/// Computes the current collateral ratio (CR) of the protocol.
/// `CR = total_sol_usd / stablecoin_cap`
Expand Down
2 changes: 2 additions & 0 deletions hylo-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ pub mod fee_controller;
#[cfg(feature = "offchain")]
pub mod idl_type_bridge;
pub mod lst_sol_price;
pub mod oracle;
pub mod pyth;
pub mod slippage_config;
pub mod switchboard;
pub mod solana_clock;
pub mod stability_mode;
pub mod stability_pool_math;
Expand Down
157 changes: 157 additions & 0 deletions hylo-core/src/oracle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//! Unified oracle interface supporting both Pyth and Switchboard oracles.
//!
//! This module provides a trait-based interface for querying prices from different
//! oracle providers. The `OraclePrice` trait can be implemented by any oracle type,
//! allowing for extensibility and testing with mock oracles.
//!
//! # Examples
//!
//! ```ignore
//! use hylo_core::oracle::{OraclePrice, OracleConfig};
//! use fix::typenum::N8;
//! use fix::prelude::*;
//!
//! // Configure oracle settings
//! let config = OracleConfig::new(
//! 60, // 60 second staleness tolerance
//! UFix64::<N8>::from_num(0.01), // 1% confidence tolerance
//! );
//!
//! // Works with any oracle type that implements OraclePrice
//! let price = pyth_oracle.query_price(&clock, config)?;
//! let price = switchboard_quote.query_price(&clock, config)?;
//!
//! // Or pass as a generic
//! fn get_price<O: OraclePrice>(oracle: &O, clock: &impl SolanaClock) -> Result<PriceRange<N8>> {
//! oracle.query_price(clock, config)
//! }
//! ```

use anchor_lang::prelude::Result;
use fix::prelude::*;
use fix::typenum::Integer;
use pyth_solana_receiver_sdk::price_update::PriceUpdateV2;
use switchboard_on_demand::SwitchboardQuote;

use crate::solana_clock::SolanaClock;

// Re-export commonly used types for convenience
pub use crate::pyth::{query_pyth_price, OracleConfig, PriceRange};
pub use crate::switchboard::query_switchboard_price;

/// Trait for querying oracle prices.
///
/// This trait can be implemented for any oracle type, allowing for:
/// - Multiple oracle providers (Pyth, Switchboard, etc.)
/// - Mock oracles for testing
/// - Custom oracle implementations
pub trait OraclePrice {
/// Query the current price from this oracle with validations.
///
/// # Arguments
/// * `clock` - Clock implementation for getting current slot/time
/// * `config` - Oracle configuration with staleness interval and confidence tolerance
///
/// # Returns
/// A `PriceRange` with lower and upper bounds for the asset price
fn query_price<Exp: Integer, C: SolanaClock>(
&self,
clock: &C,
config: OracleConfig<Exp>,
) -> Result<PriceRange<Exp>>
where
UFix64<Exp>: FixExt;
}

/// Implementation of OraclePrice for Pyth's PriceUpdateV2
impl OraclePrice for PriceUpdateV2 {
fn query_price<Exp: Integer, C: SolanaClock>(
&self,
clock: &C,
config: OracleConfig<Exp>,
) -> Result<PriceRange<Exp>>
where
UFix64<Exp>: FixExt,
{
query_pyth_price(clock, self, config)
}
}

/// Implementation of OraclePrice for Switchboard's SwitchboardQuote
impl OraclePrice for SwitchboardQuote {
fn query_price<Exp: Integer, C: SolanaClock>(
&self,
clock: &C,
config: OracleConfig<Exp>,
) -> Result<PriceRange<Exp>>
where
UFix64<Exp>: FixExt,
{
query_switchboard_price(clock, self, config)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::solana_clock::SolanaClock;
use fix::typenum::N8;

// Mock oracle for testing
struct MockOracle {
lower: u64,
upper: u64,
}

impl OraclePrice for MockOracle {
fn query_price<Exp: Integer, C: SolanaClock>(
&self,
_clock: &C,
_config: OracleConfig<Exp>,
) -> Result<PriceRange<Exp>>
where
UFix64<Exp>: FixExt,
{
Ok(PriceRange {
lower: UFix64::new(self.lower),
upper: UFix64::new(self.upper),
})
}
}

#[test]
fn test_trait_extensibility() {
// Test that we can create custom oracle implementations
struct TestClock;
impl SolanaClock for TestClock {
fn slot(&self) -> u64 {
100
}
fn epoch(&self) -> u64 {
10
}
fn epoch_start_timestamp(&self) -> i64 {
0
}
fn leader_schedule_epoch(&self) -> u64 {
10
}
fn unix_timestamp(&self) -> i64 {
1000000
}
}

let mock = MockOracle {
lower: 10000000000, // $100 with 8 decimals
upper: 10100000000, // $101 with 8 decimals
};
let config = OracleConfig::new(60, UFix64::<N8>::new(1000000)); // 1% tolerance
let clock = TestClock;

let result = mock.query_price(&clock, config);
assert!(result.is_ok());
let price_range = result.unwrap();
assert_eq!(price_range.lower, UFix64::<N8>::new(10000000000));
assert_eq!(price_range.upper, UFix64::<N8>::new(10100000000));
}
}
2 changes: 1 addition & 1 deletion hylo-core/src/stability_pool_math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::error::CoreError::{
LpTokenNav, LpTokenOut, StabilityPoolCap, StablecoinToSwap, TokenWithdraw,
};
use crate::fee_controller::FeeExtract;
use crate::pyth::PriceRange;
use crate::oracle::PriceRange;

/// Calculates total dollar value of stablecoin and levercoin in stability pool.
///
Expand Down
Loading