Stale Oracle Data Exploit Generator
Sigvex exploit generator that validates Chainlink 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 Chainlink 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 Chainlink selectors to confirm that Chainlink 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 Chainlink prices.
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
Stale price exploitation:
- Chainlink’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:
- Chainlink 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 Chainlink selectors and records the oracle type accordingly.
Three execution scenarios configure the simulated oracle storage using the Chainlink 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 Chainlink 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 Chainlink 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 (Chainlink + Uniswap TWAP) and halt operations if they diverge by more than a threshold.
References
- Chainlink: latestRoundData Documentation
- Chainlink: L2 Sequencer Uptime Feeds
- Venus Protocol Exploit Analysis ($100M+ losses from stale oracle)
- SWC-116: Block Values as Proxy for Time