Price Validation
getPrice() runs three checks before returning a value. Each one corresponds
to a real failure mode observed in production.
function getPrice() external view returns (int256) { if (sequencerUptimeFeed != address(0)) _checkSequencer();
(uint80 roundId, int256 price, , uint256 updatedAt, uint80 answeredInRound) = priceFeed.latestRoundData();
if (price <= 0) revert InvalidPrice();
if (updatedAt == 0 || answeredInRound < roundId || block.timestamp - updatedAt > stalenessThreshold) { revert StalePrice(updatedAt); }
if (maxDeviationBps > 0) { uint256 deviation = _calcDeviation(lastAcceptedPrice, price); if (deviation > maxDeviationBps) revert DeviationExceeded(deviation); }
return price;}Check 1: Price sign
if (price <= 0) revert InvalidPrice();latestRoundData returns int256, but a price feed reporting zero or a
negative value is broken. This happens when an aggregator is freshly deployed
and has not yet received its first answer, or when a feed is deprecated and
has stopped updating.
This check runs before the staleness check deliberately. Zero and negative prices are not a staleness problem. They indicate a broken aggregator, not a slow one. Conflating the two failure modes into one error makes monitoring harder and hides the real cause.
Check 2: Round completeness and staleness
if (updatedAt == 0 || answeredInRound < roundId || block.timestamp - updatedAt > stalenessThreshold) { revert StalePrice(updatedAt);}Three conditions, one revert. Each catches a different way a round can be untrustworthy.
updatedAt == 0: an aggregator that has never successfully completed
a round returns zero for updatedAt. Without this guard, the subtraction
block.timestamp - updatedAt would wrap around to an enormous number and
pass the staleness check incorrectly.
answeredInRound < roundId: This guards against incomplete rounds.
In some aggregator configurations, a new round ID can be initialized before
the previous round’s data is fully finalized and moved to the “latest” state.
If answeredInRound is less than the current roundId, it indicates the oracle network
is currently in the process of reaching consensus, and the returned price belongs
to a superseded round. Relying on this data during high volatility can lead to using
“stale” data that hasn’t yet caught up to the current market round.
block.timestamp - updatedAt > stalenessThreshold: the heartbeat check.
Every feed has a documented update frequency. A round older than that window
means the feed has stopped keeping up, and the price should not be trusted.
What latestRoundData returns
Understanding the return tuple is necessary for correct integration:
( uint80 roundId, // Current round identifier int256 answer, // The price uint256 startedAt, // When the round started uint256 updatedAt, // When the round was last updated (use this for staleness) uint80 answeredInRound // The round in which the answer was computed)startedAt is when the round was initiated, not when it was answered.
Always use updatedAt for staleness checks. startedAt can predate the
actual answer significantly during periods of low aggregator activity.
Custom errors
error InvalidPrice();error StalePrice(uint256 updatedAt);Custom errors are cheaper than require strings at revert time and carry
structured data. StalePrice includes updatedAt so callers can determine
exactly how old the rejected round was, useful for monitoring and alerting.