Single Oracle Dependency Exploit Generator
Sigvex exploit generator that validates single oracle dependency vulnerabilities by simulating primary oracle failure with no fallback, confirming protocols that halt or misbehave when the sole price source goes offline.
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 Chainlink feeds 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 Chainlink feed for ETH/USD pricing.
- The Chainlink 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: Chainlink + Uniswap V3 TWAP
// Use a 30-minute TWAP to detect Chainlink manipulation
uint256 chainlinkPrice = getChainlinkPrice();
uint256 twapPrice = uniswapV3Oracle.consult(token, 1800); // 30-min TWAP
require(abs(chainlinkPrice - twapPrice) <= chainlinkPrice * 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 = [
chainlinkPrice(),
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 Chainlink price. A sequencer outage means the Chainlink feed may be stale while still passing freshness checks.