Single Oracle Dependency Exploit Generator
Overview
The single oracle dependency exploit generator validates findings from the single_oracle_dependency, oracle_single_source, and related detectors by executing the target contract under two oracle configurations: primary oracle working normally and primary oracle returning zero (failed). If the contract proceeds with a zero price or reverts without a graceful fallback, the single point of failure is confirmed.
Relying on a single price oracle creates a critical single point of failure. When aggregated price feed pause during high volatility, when a TWAP oracle is manipulated, or when a custom oracle contract is exploited, any protocol with no fallback source is immediately at risk. Venus Protocol suffered $100M+ in losses when its BNB price oracle was manipulated; the protocol had no secondary source to cross-reference. A single manipulated or failed oracle can either drain a protocol (bad price accepted) or brick it (revert on every operation).
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
Oracle failure with no fallback:
- The protocol uses a single aggregated price feed for ETH/USD pricing.
- The aggregated price feed’s heartbeat expires during a network congestion event or L2 sequencer outage.
latestRoundData()returns a stale price; the protocol’s staleness check causes every transaction to revert.- Users cannot withdraw funds, repay loans, or close positions for the duration of the outage.
- If positions become undercollateralized during the outage, liquidators also cannot act.
Oracle manipulation with no cross-check:
- An attacker manipulates the on-chain price oracle (flash loan attack on a spot price oracle).
- With a single source, the protocol has no way to detect the manipulation.
- The attacker borrows against inflated collateral or triggers cascading liquidations.
- A secondary oracle (TWAP or alternative feed) with a deviation check would have rejected the manipulated price.
Exploit Mechanics
The generator executes two scenarios against a simulated oracle address, with price stored at slot 1:
| Scenario | Oracle slot 1 value | Expected behavior | Confirmed if |
|---|---|---|---|
| 1 — Working | 200000000000 ($2000, 8 dec) |
Executes successfully | Baseline |
| 2 — Failed | 0 (zero price) |
Should gracefully fallback | Succeeds or reverts without fallback (0.90 confidence) |
The fallback selector 0x50d25bcd (latestAnswer) is used when no specific selector is available from the finding location.
Verdict:
- Working succeeds and Failed succeeds → zero price accepted (confidence 0.90): protocol proceeds on bad data.
- Working succeeds and Failed reverts with no fallback path → single point of failure (confidence 0.90): no alternative oracle.
// VULNERABLE: Single oracle, no fallback
contract SingleOracleLending {
AggregatorV3Interface public immutable priceOracle;
function getCollateralValue(address user) public view returns (uint256) {
(, int256 price,,,) = priceOracle.latestRoundData();
// If priceOracle fails, this entire function reverts
// There is no fallback — the protocol is bricked
return uint256(collateral[user]) * uint256(price) / 1e8;
}
}
// SECURE: Primary + fallback oracle with cross-validation
contract MultiOracleLending {
AggregatorV3Interface public primaryOracle;
ITwapOracle public fallbackOracle;
uint256 constant DEVIATION_THRESHOLD = 200; // 2% in basis points
function getCollateralValue(address user) public view returns (uint256) {
uint256 price = _getSafePrice();
return uint256(collateral[user]) * price / 1e8;
}
function _getSafePrice() internal view returns (uint256) {
uint256 primaryPrice = _tryPrimaryOracle();
uint256 fallbackPrice = fallbackOracle.consult();
if (primaryPrice == 0) {
return fallbackPrice; // Use fallback if primary fails
}
// Cross-validate: reject if deviation exceeds threshold
uint256 deviation = primaryPrice > fallbackPrice
? (primaryPrice - fallbackPrice) * 10000 / primaryPrice
: (fallbackPrice - primaryPrice) * 10000 / fallbackPrice;
require(deviation <= DEVIATION_THRESHOLD, "Oracle deviation too large");
return primaryPrice;
}
function _tryPrimaryOracle() internal view returns (uint256) {
try primaryOracle.latestRoundData() returns (
uint80 roundId, int256 price, , uint256 updatedAt, uint80 answeredInRound
) {
if (price > 0
&& updatedAt >= block.timestamp - 3600
&& answeredInRound >= roundId) {
return uint256(price);
}
} catch {}
return 0; // Signal failure
}
}
Remediation
- Detector: Single Oracle Dependency Detector
- Remediation Guide: Single Oracle Dependency Remediation
Eliminate single oracle dependency with a multi-source strategy:
// Strategy 1: the off-chain aggregator + a concentrated-liquidity AMM TWAP
// Use a 30-minute TWAP to detect the off-chain aggregator manipulation
uint256 oraclePrice = getOraclePrice();
uint256 twapPrice = uniswapV3Oracle.consult(token, 1800); // 30-min TWAP
require(abs(oraclePrice - twapPrice) <= oraclePrice * MAX_DEVIATION / 10000);
// Strategy 2: Circuit breaker on oracle failure
modifier oracleHealthy() {
require(!oracleCircuitBroken, "Oracle circuit broken");
_;
}
// Strategy 3: Median of three oracles
// Resistant to any single oracle being manipulated
function medianPrice() internal view returns (uint256) {
uint256[3] memory prices = [
oraclePrice(),
bandProtocolPrice(),
uniswapTwapPrice()
];
// Sort and return median
if (prices[0] > prices[1]) (prices[0], prices[1]) = (prices[1], prices[0]);
if (prices[1] > prices[2]) (prices[1], prices[2]) = (prices[2], prices[1]);
if (prices[0] > prices[1]) (prices[0], prices[1]) = (prices[1], prices[0]);
return prices[1]; // Median
}
For L2 deployments, always check the sequencer uptime feed before reading any the off-chain aggregator price. A sequencer outage means the aggregated price feed may be stale while still passing freshness checks.