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:
- the aggregator’s ETH/USD feed stops updating due to a sequencer outage on an L2 or a deviation threshold not being met.
- The on-chain price remains at the last updated value (e.g., $1500) while the real market price has moved to $2000.
- A lending protocol that uses the stale price accepts the outdated $1500 collateral valuation.
- An attacker borrows against this artificially undervalued collateral or triggers profitable liquidations.
- Positions that would be safely collateralized at the real $2000 price are liquidated at the stale $1500 price.
Incomplete round exploitation:
- the off-chain aggregator begins a new round (
roundId = 100) but theansweredInRoundremains at 99 (incomplete). - The contract reads the answer but does not check
answeredInRound >= roundId. - The returned price is from the previous, now-stale round (round 99).
- 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
updatedAtvalidation. - Fresh succeeds and Incomplete succeeds → incomplete round accepted (confidence 0.85): no
answeredInRound >= roundIdcheck. - 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
- Detector: Stale Oracle Detector
- Remediation Guide: Stale Oracle 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
- Venus Protocol Exploit Analysis ($100M+ losses from stale oracle)
- SWC-116: Block Values as Proxy for Time