Skip to main content
Sigvex

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 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:

  1. The protocol uses a single aggregated price feed for ETH/USD pricing.
  2. The aggregated price feed’s heartbeat expires during a network congestion event or L2 sequencer outage.
  3. latestRoundData() returns a stale price; the protocol’s staleness check causes every transaction to revert.
  4. Users cannot withdraw funds, repay loans, or close positions for the duration of the outage.
  5. If positions become undercollateralized during the outage, liquidators also cannot act.

Oracle manipulation with no cross-check:

  1. An attacker manipulates the on-chain price oracle (flash loan attack on a spot price oracle).
  2. With a single source, the protocol has no way to detect the manipulation.
  3. The attacker borrows against inflated collateral or triggers cascading liquidations.
  4. 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

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.