Skip to content

Architecture

PriceFeedConsumer is a thin, auditable wrapper around AggregatorV3Interface. The design prioritizes correctness over flexibility: the feed address is fixed at deployment, validation is synchronous and view-only, and all mutable parameters are owner-controlled with no time delay.

Contract at a glance

contract PriceFeedConsumer is Ownable {
AggregatorV3Interface public immutable priceFeed;
address public sequencerUptimeFeed;
uint256 public stalenessThreshold;
uint256 public maxDeviationBps;
int256 public lastAcceptedPrice;
}

Why priceFeed is immutable

The feed address is the most critical dependency in the contract. Making it immutable eliminates an entire class of attack: there is no setFeed function for an attacker or compromised owner to redirect price reads to a malicious aggregator.

If you need to upgrade the feed (for example when Chainlink deprecates a proxy), deploy a new PriceFeedConsumer and migrate. The cost of one additional deployment is worth the reduction in attack surface.

sequencerUptimeFeed is deliberately mutable. On L1 deployments it stays at address(0), and the check is skipped. On L2 deployments it is set post-construction via setSequencerUptimeFeed. This makes the same contract deployable across both environments without branching the code.

Constructor initialization

constructor(address _feed, uint256 _threshold, uint256 _deviation) Ownable() {
priceFeed = AggregatorV3Interface(_feed);
stalenessThreshold = _threshold;
maxDeviationBps = _deviation;
(, lastAcceptedPrice, , , ) = priceFeed.latestRoundData();
if (lastAcceptedPrice <= 0) revert InvalidPrice();
}

The constructor reads an initial price from the feed and sets it as the deviation baseline. There’s no separate acceptPrice call needed after deployment. The contract is ready to use the moment construction succeeds.

The validity check on lastAcceptedPrice does more than validate the price. If the feed address is wrong, the aggregator is uninitialized, or the feed has been deprecated, the constructor reverts. The entire deployment transaction fails cleanly rather than leaving a misconfigured contract sitting on-chain waiting to cause problems.

Parameter ownership

stalenessThreshold and maxDeviationBps share a single setter:

function setParams(uint256 _threshold, uint256 _deviation) external onlyOwner {
stalenessThreshold = _threshold;
maxDeviationBps = _deviation;
}

One call instead of two. The gas saving is minor, but the design reason is more practical: these parameters are always tuned together in production. Tightening the staleness window usually means tightening the deviation threshold at the same time. A single setter makes it impossible to update one and forget the other.

Constants

uint256 public constant BPS_BASE = 10_000;
uint256 public constant GRACE_PERIOD_TIME = 3600;

BPS_BASE is public so consuming contracts can reference it directly rather than hardcoding 10_000 in their own logic. GRACE_PERIOD_TIME is fixed. It is not a parameter because a shorter grace period would defeat the purpose of having one. Chainlink’s own guidance is one hour, and that is what this contract enforces.