Oracle Failure Exploit Generator
Sigvex exploit generator that validates oracle failure vulnerabilities by testing contract behavior when price feeds return zero, extreme, or unavailable values.
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 (Chainlink + Uniswap 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.