Oracle Manipulation
Detects Solana programs that rely on unvalidated or manipulable price oracle data, enabling attackers to trigger liquidations, drain vaults, or manipulate DeFi calculations.
Oracle Manipulation
Overview
Remediation Guide: How to Fix Oracle Manipulation
The oracle-manipulation detector identifies Solana programs that read price data or other off-chain values from oracle accounts without validating the data’s freshness, the oracle account’s authority, or the data’s plausibility bounds. DeFi protocols that use price oracles for collateral valuation, liquidation thresholds, swap rates, or borrow limits are vulnerable when the oracle data can be manipulated, stale, or spoofed.
Sigvex tracks oracle account reads through the HIR dataflow graph, identifying patterns where: (a) price data is read from an account without checking the oracle account’s program owner or authority, (b) the price update timestamp is not validated against a staleness threshold, or (c) the price value is used directly in financial calculations without sanity bounds checking.
The detector is sensitive to both Pyth Network and Switchboard oracle patterns on Solana, as these are the most widely used oracle solutions in the Solana DeFi ecosystem.
Why This Is an Issue
Oracle manipulation in Solana DeFi can take several forms:
Stale price attacks: An oracle price that has not been updated recently may reflect market conditions from minutes or hours ago. During high-volatility periods, stale prices can be exploited to borrow against overvalued collateral or trigger liquidations of healthy positions.
Oracle account spoofing: If a program does not verify that the oracle account is owned by the expected oracle program (Pyth, Switchboard), an attacker can create a fake account with the same data layout and pass it to the program, providing arbitrary price data.
Single-oracle dependency: Protocols relying on a single oracle have a single point of failure. If the oracle operator is compromised or goes offline, the protocol is either stopped (if it detects staleness) or exploitable with the last-known stale price.
Price deviation exploitation: In protocols without price sanity checks, a temporary large deviation in oracle price (due to market volatility or oracle malfunction) can trigger mass liquidations or enable profitable exploits.
The Mango Markets exploit ($117M, October 2022) involved oracle price manipulation on Solana — the attacker used large spot positions to temporarily move the price oracle, enabling undercollateralized borrowing.
How to Resolve
For Pyth Network oracles, always validate price confidence intervals, staleness, and the oracle account’s owner:
// Before: Vulnerable — reads price without validation
use pyth_sdk_solana::Price;
pub fn get_price(accounts: &[AccountInfo]) -> Result<i64, ProgramError> {
let price_account = &accounts[0];
// VULNERABLE: No owner check, no staleness check, no confidence check
let price_feed = pyth_sdk_solana::load_price_feed_from_account_info(price_account)
.map_err(|_| ProgramError::InvalidAccountData)?;
Ok(price_feed.get_price_unchecked().price)
}
// After: Fixed — validate oracle account and data quality
use pyth_sdk_solana::{load_price_feed_from_account_info, Price};
use solana_program::clock::Clock;
const MAX_PRICE_STALENESS_SECONDS: i64 = 60; // 1 minute maximum age
const MAX_CONFIDENCE_RATIO: u64 = 100; // Confidence must be < 1% of price
pub fn get_validated_price(
accounts: &[AccountInfo],
pyth_program_id: &Pubkey,
) -> Result<i64, ProgramError> {
let price_account = &accounts[0];
// Verify account is owned by Pyth
if price_account.owner != pyth_program_id {
msg!("Price account not owned by Pyth program");
return Err(ProgramError::InvalidAccountData);
}
let price_feed = load_price_feed_from_account_info(price_account)
.map_err(|_| ProgramError::InvalidAccountData)?;
let clock = Clock::get()?;
let current_time = clock.unix_timestamp;
// Validate price is fresh
let price = price_feed
.get_price_no_older_than(current_time, MAX_PRICE_STALENESS_SECONDS)
.ok_or_else(|| {
msg!("Price data is stale");
ProgramError::InvalidAccountData
})?;
// Validate confidence interval (price uncertainty)
let confidence_ratio = price.conf as u64 * 10000 / (price.price.unsigned_abs() as u64 + 1);
if confidence_ratio > MAX_CONFIDENCE_RATIO {
msg!("Price confidence interval too wide: {}bps", confidence_ratio);
return Err(ProgramError::InvalidAccountData);
}
Ok(price.price)
}
For Switchboard oracles:
use switchboard_v2::AggregatorAccountData;
const MAX_STALENESS_SLOTS: u64 = 150; // ~1 minute at 400ms slot times
pub fn get_switchboard_price(
accounts: &[AccountInfo],
switchboard_program_id: &Pubkey,
) -> Result<f64, ProgramError> {
let aggregator_account = &accounts[0];
// Verify account ownership
if aggregator_account.owner != switchboard_program_id {
return Err(ProgramError::InvalidAccountData);
}
let aggregator = AggregatorAccountData::new(aggregator_account)?;
// Check staleness
let current_slot = Clock::get()?.slot;
if current_slot.saturating_sub(aggregator.latest_confirmed_round.round_open_slot) > MAX_STALENESS_SLOTS {
return Err(ProgramError::InvalidAccountData);
}
let result = aggregator.get_result()?;
Ok(f64::try_from(result)?)
}
Examples
Vulnerable Code
// Lending protocol that borrows against collateral using unvalidated oracle prices
pub fn borrow(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let user = &accounts[0];
let collateral_oracle = &accounts[1]; // Not validated!
let user_position = &accounts[2];
// No owner check on oracle account
let price_data = OracleData::try_from_slice(&collateral_oracle.data.borrow())?;
// No staleness check — price could be hours old
let collateral_value = price_data.price * user_position_data.collateral_amount;
let max_borrow = collateral_value * 7 / 10; // 70% LTV
if amount > max_borrow {
return Err(ProgramError::InvalidArgument);
}
// Execute borrow...
Ok(())
}
Fixed Code
const PYTH_MAINNET_PROGRAM: &str = "FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH";
const MAX_ORACLE_AGE_SECS: i64 = 30;
pub fn borrow(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let user = &accounts[0];
let collateral_oracle = &accounts[1];
let user_position = &accounts[2];
let pyth_program = Pubkey::from_str(PYTH_MAINNET_PROGRAM).unwrap();
// Verify oracle ownership
if collateral_oracle.owner != &pyth_program {
msg!("Invalid oracle account owner");
return Err(ProgramError::InvalidAccountData);
}
let price_feed = load_price_feed_from_account_info(collateral_oracle)?;
let clock = Clock::get()?;
// Get price with staleness check
let price = price_feed
.get_price_no_older_than(clock.unix_timestamp, MAX_ORACLE_AGE_SECS)
.ok_or(ProgramError::InvalidAccountData)?;
let position = UserPosition::try_from_slice(&user_position.data.borrow())?;
let collateral_value = (price.price as u64) * position.collateral_amount;
let max_borrow = collateral_value * 7 / 10;
if amount > max_borrow {
return Err(ProgramError::InvalidArgument);
}
Ok(())
}
Sample Sigvex Output
{
"detector_id": "oracle-manipulation",
"severity": "critical",
"confidence": 0.81,
"description": "Function borrow() reads price data from account collateral_oracle without verifying the account's owner program or the price timestamp. A spoofed oracle account could enable unlimited borrowing.",
"location": { "function": "borrow", "offset": 16 }
}
Detection Methodology
The detector performs structured oracle validation analysis:
- Oracle account identification: Identifies accounts that are deserialized as oracle data structures (Pyth
PriceFeed, SwitchboardAggregatorAccountData, or custom price account schemas). - Owner validation check: Verifies that the oracle account’s owner (program ID) is compared against an expected oracle program ID before the price data is read.
- Staleness check detection: Looks for timestamp comparisons between the price data’s update time and the current clock that would enforce a maximum staleness limit.
- Confidence interval check: For Pyth feeds, checks whether the
conffield is validated relative toprice. - Downstream usage: Tracks the oracle-derived price value into financial calculations (multiplication, division for LTV ratios, comparisons for liquidation thresholds) to confirm high-impact usage.
Limitations
False positives:
- Programs that read oracle data only for informational purposes (logging, events) without using it in financial logic will produce lower-confidence findings.
- Custom oracle implementations with non-standard validation patterns may be flagged even when correctly validating data.
False negatives:
- Oracle validation performed in a prior instruction of a multi-instruction transaction will not be recognized.
- Programs that use TWAP calculations over multiple oracle reads may not be correctly identified as having staleness protection.
Related Detectors
- Weak Randomness — detects use of predictable on-chain values as randomness
- CPI Program Validation — detects unvalidated CPI program IDs