Skip to main content
Sigvex

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:

  1. A price oracle returns 0 due to a bug, an uninitialized aggregator, or a decimals mismatch.
  2. The lending protocol computes collateral value as balance * price / 1e18 = 0.
  3. All positions appear to be worth nothing, triggering mass liquidations.
  4. Alternatively, in a borrow context, an attacker borrows against zero-priced collateral for free.

Extreme price acceptance:

  1. An oracle returns type(uint256).max (2^256 - 1) due to overflow in an intermediary contract.
  2. The protocol computes collateral at astronomical value, allowing unlimited borrowing.
  3. The attacker borrows the entire protocol treasury against a small collateral amount.
  4. The attacker repays nothing — the loan is larger than all assets in existence.

Oracle unavailability:

  1. A price feed is deprecated and its storage is zeroed out or the contract is removed.
  2. The protocol’s getPrice() call returns empty data or 0.
  3. Contracts with no fallback mechanism are unable to process any transactions.
  4. 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 > 0 validation.
  • 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

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.

References