Oracle staleness is one of the most prevalent vulnerability classes in DeFi lending protocols. When a protocol fails to validate the freshness of price data from Chainlink oracles, it creates opportunities for exploitation during market stress, network congestion, or oracle downtime.
This research documents a systematic vulnerability pattern identified across multiple Aave-derived lending protocols, demonstrating how this single oversight can lead to catastrophic financial losses.
During Black Thursday (March 2020), Chainlink oracles experienced significant delays due to network congestion. Protocols with proper staleness checks paused operations; those without suffered millions in losses.
Chainlink provides two primary functions for fetching price data. The vulnerable pattern uses the
deprecated latestAnswer() function or ignores validation fields
from latestRoundData().
// VULNERABLE: No staleness validation
function getAssetPrice(address asset) external view returns (uint256) {
int256 price = source.latestAnswer(); // Deprecated!
if (price > 0) {
return uint256(price);
}
return fallbackOracle.getAssetPrice(asset);
}
The code above has multiple critical flaws:
latestAnswer() is deprecated by Chainlink// latestRoundData() returns 5 values
(
uint80 roundId, // Current round identifier
int256 answer, // The price
uint256 startedAt, // When this round started
uint256 updatedAt, // When this round was last updated
uint80 answeredInRound // Which round the answer came from
) = priceFeed.latestRoundData();
Our analysis identified this vulnerability pattern across multiple major lending protocols:
| Protocol | TVL | latestRoundData | updatedAt | Max Staleness | L2 Sequencer |
|---|---|---|---|---|---|
| Aave V4 | TBD | Yes | No | No | No |
| SparkLend | $2B+ | latestAnswer | No | No | No |
| Radiant | $100M+ | latestAnswer | No | No | No |
| Moonwell | $50M+ | Yes | Partial | No | No |
Uses deprecated latestAnswer() without any validation.
Direct fork of Aave V3 inheriting the same oracle pattern.
Bounty Platform: Immunefi | Max Payout: $5,000,000
Cross-chain lending protocol on Arbitrum. Uses same vulnerable oracle pattern. No staleness validation on any supported chain.
Bounty Platform: Immunefi | Previously hacked: $4.5M (2024)
The reverse scenario is equally damaging. If the oracle is stale-low (real price higher), healthy positions get liquidated at incorrect valuations, directly harming users.
A properly secured oracle integration must validate all returned data:
function getPrice(address priceFeed) internal view returns (uint256) {
(
uint80 roundId,
int256 answer,
,
uint256 updatedAt,
uint80 answeredInRound
) = AggregatorV3Interface(priceFeed).latestRoundData();
// Validate price is positive
require(answer > 0, "Invalid price");
// Validate round is complete
require(updatedAt > 0, "Round not complete");
// Validate answer is from current round
require(answeredInRound >= roundId, "Stale round");
// Validate staleness threshold (e.g., 1 hour for most feeds)
require(
block.timestamp - updatedAt < STALENESS_THRESHOLD,
"Price too old"
);
return uint256(answer);
}
Different assets have different heartbeat frequencies. ETH/USD updates every ~1 hour, while some exotic pairs may only update every 24 hours. Configure staleness thresholds per asset.
On Layer 2 networks (Arbitrum, Optimism, Base), an additional check is required: the sequencer uptime feed. When the L2 sequencer goes down, oracle updates stop but the last price remains.
function checkSequencer() internal view {
(
,
int256 answer,
uint256 startedAt,
,
) = sequencerFeed.latestRoundData();
// answer == 0: Sequencer is up
// answer == 1: Sequencer is down
require(answer == 0, "Sequencer down");
// Grace period after sequencer comes back up
require(
block.timestamp - startedAt > GRACE_PERIOD,
"Grace period not passed"
);
}
When auditing Chainlink oracle integrations, verify:
latestRoundData() not latestAnswer()answer > 0updatedAt > 0answeredInRound >= roundId