Normal behavior:
A secure oracle wrapper should validate that price data is fresh before returning it to downstream protocols, rejecting stale values that could misrepresent current market conditions
problem
The getPrice() function returns price data from Chainlink feeds without validating the updatedAt timestamp. This allows the contract to serve arbitrarily old prices if the aggregator stops updating (due to network issues, node failures, or deliberate manipulation), causing downstream protocols to make critical decisions on outdated market data.
https:
function getPrice(address _token) public view returns (uint256 price) {
address priceFeedAddress = priceFeeds[_token];
require(priceFeedAddress != address(0), "Price feed not set for token");
AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeedAddress);
@> (, int256 answer,,,) = priceFeed.latestRoundData();
require(answer > 0, "Invalid price from oracle");
price = uint256(answer);
}
Risk
Likelihood: High
Chainlink feeds have frozen multiple times in production
Aggregators can stop updating during extreme market volatility when accurate pricing is most critical
No validation means any frozen feed automatically becomes exploitable
impact
Downstream lending protocols may execute incorrect liquidations using stale prices (e.g., liquidating healthy positions during flash crashes)
AMMs could suffer value extraction via stale-price arbitrage
Proof of Concept
contract VulnerableLendingProtocol {
StrataxOracle public oracle;
mapping(address => uint256) public collateral;
function liquidate(address user) external {
uint256 ethPrice = oracle.getPrice(ETH_TOKEN);
uint256 collateralValue = collateral[user] * ethPrice / 1e18;
require(collateralValue < debt[user] * 1.5e18, "Not liquidatable");
seizeCollateral(user);
}
}
Recommended Mitigation
-
+
- (, int256 answer,,,) = priceFeed.latestRoundData();
- require(answer > 0, "Invalid price from oracle");
+ (, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = priceFeed.latestRoundData();
+ require(answer > 0, "Invalid price");
+ require(updatedAt != 0, "Never updated");
+ require(block.timestamp - updatedAt <= maxStaleness, "Stale price");
+ require(answeredInRound >= answeredInRound, "Incomplete round"); // Defense-in-depth
price = uint256(answer);
}
}