Oracle Failure Exploit Generator
Overview
The oracle failure exploit generator validates findings from the oracle_failure, price_feed_failure, and related detectors by executing the target contract under four oracle conditions: normal operation ($2000), zero price return, extreme price return (2^256 - 1), and oracle unavailability (empty storage). If the contract accepts invalid price data without reverting, the vulnerability is confirmed.
Oracle failures are distinct from staleness — the feed is responsive but returns pathological values: zero from a decimals mismatch or contract bug, an astronomically large number from an overflow, or no data at all due to feed deprecation. The Terra/UST depeg (May 2022) demonstrated how oracle failures cascade: the on-chain price oracle reported UST at $1.00 while the market had moved to $0.10, enabling massive arbitrage and accelerating the death spiral. Protocols that accepted zero or near-zero prices without halting suffered complete fund loss.
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
Zero price acceptance:
- A price oracle returns
0due to a bug, an uninitialized aggregator, or a decimals mismatch. - The lending protocol computes collateral value as
balance * price / 1e18 = 0. - All positions appear to be worth nothing, triggering mass liquidations.
- Alternatively, in a borrow context, an attacker borrows against zero-priced collateral for free.
Extreme price acceptance:
- An oracle returns
type(uint256).max(2^256 - 1) due to overflow in an intermediary contract. - The protocol computes collateral at astronomical value, allowing unlimited borrowing.
- The attacker borrows the entire protocol treasury against a small collateral amount.
- The attacker repays nothing — the loan is larger than all assets in existence.
Oracle unavailability:
- A price feed is deprecated and its storage is zeroed out or the contract is removed.
- The protocol’s
getPrice()call returns empty data or0. - Contracts with no fallback mechanism are unable to process any transactions.
- Denial of service: users cannot withdraw, deposit, or close positions.
Exploit Mechanics
The generator sets up oracle storage at slot 1 (price slot) with four configurations:
| Scenario | Price value | Expected behavior | Confirmed if |
|---|---|---|---|
| 1 — Normal | 200000000000 ($2000, 8 dec) |
Executes successfully | Baseline |
| 2 — Zero price | 0 |
Should revert | Succeeds (0.90 confidence) |
| 3 — Extreme price | 2^256 - 1 (maximum uint256) |
Should revert | Succeeds (0.85 confidence) |
| 4 — Unavailable | Empty storage (no state) | Should revert | Succeeds (0.70 confidence) |
The generator configures a simulated oracle address for each scenario. The fallback selector 0x50d25bcd (latestAnswer) is used when no specific selector is available from the finding location.
Verdict:
- Normal succeeds and Zero succeeds → zero price accepted (confidence 0.90): no
price > 0validation. - Normal succeeds and Extreme succeeds → extreme price accepted (confidence 0.85): no upper-bound check.
- Normal succeeds and Unavailable succeeds → no fallback (confidence 0.70): contract proceeds without valid data.
// VULNERABLE: No price validation
contract VulnerableLending {
function borrow(uint256 amount) external {
uint256 price = oracle.latestAnswer();
// price could be 0 or type(uint256).max — no check!
uint256 collateralValue = collateral * price / 1e8;
require(collateralValue >= amount, "Undercollateralized");
_transfer(msg.sender, amount);
}
}
// SECURE: Validate all price edge cases
contract SafeLending {
uint256 constant MIN_PRICE = 1e6; // $0.01 minimum
uint256 constant MAX_PRICE = 1e15; // $10,000,000 maximum
uint256 constant MAX_PRICE_AGE = 3600; // 1 hour max age
function borrow(uint256 amount) external {
(
uint80 roundId,
int256 price,
,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
require(price > 0, "Invalid price: zero or negative");
require(uint256(price) >= MIN_PRICE, "Price below minimum");
require(uint256(price) <= MAX_PRICE, "Price above maximum");
require(updatedAt >= block.timestamp - MAX_PRICE_AGE, "Stale price");
require(answeredInRound >= roundId, "Incomplete round");
uint256 collateralValue = collateral * uint256(price) / 1e8;
require(collateralValue >= amount, "Undercollateralized");
_transfer(msg.sender, amount);
}
}
Remediation
- Detector: Oracle Failure Detector
- Remediation Guide: Oracle Failure Remediation
Validate every price return value before use:
// 1. Require non-zero, positive price
require(price > 0, "Invalid price");
// 2. Require price within expected bounds
require(uint256(price) >= MIN_ACCEPTABLE_PRICE, "Price too low");
require(uint256(price) <= MAX_ACCEPTABLE_PRICE, "Price too high");
// 3. Implement a fallback oracle (the off-chain aggregator + a major AMM TWAP)
function getSafePrice() internal view returns (uint256) {
try chainlink.latestRoundData() returns (uint80, int256 price, , uint256 updatedAt, uint80) {
if (price > 0 && updatedAt >= block.timestamp - MAX_AGE) {
return uint256(price);
}
} catch {}
// Fallback to TWAP
return twapOracle.consult(token, 1e18);
}
// 4. Circuit breaker: pause if oracle fails
bool public oracleCircuitBroken;
function breakCircuit() external onlyKeeper {
oracleCircuitBroken = true;
emit CircuitBroken(block.timestamp);
}
For production protocols, implement a multi-oracle strategy: require at least two independent sources to agree within a tolerance band (e.g., 2%) before accepting any price.