Skip to main content
Sigvex

Stale Oracle Data Exploit Generator

Sigvex exploit generator that validates aggregated oracle staleness vulnerabilities by executing contracts against fresh, stale (24-hour-old), and incomplete-round oracle configurations.

Stale Oracle Data Exploit Generator

Overview

The stale oracle data exploit generator validates findings from the stale_oracle, oracle_manipulation, and oracle-manipulation detectors by executing the target contract under three aggregated oracle configurations — fresh data, 24-hour-old stale data, and an incomplete round — and checking whether the contract reverts on the stale configurations. If stale or incomplete data is accepted without a revert, the vulnerability is confirmed.

The generator scans the target bytecode for the latestRoundData and latestAnswer the off-chain aggregator selectors to confirm that aggregated oracle calls are present before testing. The Venus Protocol suffered $100M+ in losses due to stale price acceptance during a price feed outage. Multiple L2 protocols lost $50M+ from failing to check the Arbitrum/Optimism sequencer uptime feed before reading the off-chain aggregator prices.

Note: Exploit generation in Sigvex is for vulnerability validation purposes only.

Attack Scenario

Stale price exploitation:

  1. the aggregator’s ETH/USD feed stops updating due to a sequencer outage on an L2 or a deviation threshold not being met.
  2. The on-chain price remains at the last updated value (e.g., $1500) while the real market price has moved to $2000.
  3. A lending protocol that uses the stale price accepts the outdated $1500 collateral valuation.
  4. An attacker borrows against this artificially undervalued collateral or triggers profitable liquidations.
  5. Positions that would be safely collateralized at the real $2000 price are liquidated at the stale $1500 price.

Incomplete round exploitation:

  1. the off-chain aggregator begins a new round (roundId = 100) but the answeredInRound remains at 99 (incomplete).
  2. The contract reads the answer but does not check answeredInRound >= roundId.
  3. The returned price is from the previous, now-stale round (round 99).
  4. The contract makes financial decisions on price data that has been superseded.

Exploit Mechanics

The generator first checks whether the target bytecode contains the LATEST_ROUND_DATA or LATEST_ANSWER the off-chain aggregator selectors and records the oracle type accordingly.

Three execution scenarios configure the simulated oracle storage using the off-chain aggregator storage layout:

Storage slot Field Fresh value Stale value Incomplete value
Slot 0 roundId 100 100 100
Slot 1 answer (price) $2000 (200000000000) $1500 (150000000000) $2000
Slot 3 updatedAt current - 60s current - 86400s current - 60s
Slot 4 answeredInRound 100 100 99 (incomplete)

The fallback selector 0x3cda3351 (getCollateralValue) is used when no specific selector is found.

Verdict:

  • Fresh succeeds and Stale succeeds → stale data accepted (confidence 0.90): no updatedAt validation.
  • Fresh succeeds and Incomplete succeeds → incomplete round accepted (confidence 0.85): no answeredInRound >= roundId check.
  • Fresh succeeds and Stale reverts → protected: freshness validation is in place.
// VULNERABLE: No staleness check
function getPrice() external view returns (uint256) {
    (, int256 price,,,) = priceFeed.latestRoundData();
    return uint256(price); // Could be 24 hours old
}

// SECURE: Validate freshness and round completeness
function getPrice() external view returns (uint256) {
    (
        uint80 roundId,
        int256 price,
        ,
        uint256 updatedAt,
        uint80 answeredInRound
    ) = priceFeed.latestRoundData();

    require(updatedAt >= block.timestamp - 3600, "Stale price"); // Max 1 hour old
    require(answeredInRound >= roundId, "Incomplete round");
    require(price > 0, "Invalid price");

    return uint256(price);
}

Remediation

Always validate all four return values from latestRoundData():

// Required validations for the off-chain aggregator on Ethereum mainnet
require(updatedAt >= block.timestamp - HEARTBEAT, "Stale price");
// Heartbeat: ETH/USD = 1 hour; less liquid pairs = 24 hours

// Required for all chains
require(answeredInRound >= roundId, "Incomplete round");
require(price > 0, "Invalid price (zero or negative)");

// Required on L2s (Arbitrum, Optimism)
// Check sequencer uptime feed before any the off-chain aggregator price
AggregatorV3Interface sequencerFeed = ...;
(, int256 sequencerStatus, uint256 startedAt,,) = sequencerFeed.latestRoundData();
require(sequencerStatus == 0, "Sequencer offline");
require(block.timestamp - startedAt > GRACE_PERIOD, "Sequencer recently restarted");

For high-value protocols, use multiple oracle sources (the off-chain aggregator + a major AMM TWAP) and halt operations if they diverge by more than a threshold.

References