Services Approach Projects Research About Request Engagement
Fundamentals

Oracles: How a Contract Learns What Something Costs

A contract cannot see outside its own chain, so everything it knows about a price was put there by somebody. How price feeds actually update, why 234 of Chainlink's 290 Ethereum mainnet feeds tolerate a full day between updates, what a consumer is supposed to check, and what Chainlink's own example contract and Aave's price oracle actually check instead.

Intermediate Fundamentals 10 min read Aug 25, 2026

A lending protocol has to know what your collateral is worth. A perpetuals exchange has to know the mark price. Neither of them can access data outside the network in order to do so.

A contract can read its own storage and anything else on its deployed chain. It cannot make an HTTP request, read a file, or check an exchange.

All figures below are read from published documentation and live contract data on 17 and 18 August 2026.

Why a contract cannot just fetch the price

The reason is determinism, and it is not a limitation anyone is working to remove.

Every node on the network executes every transaction and must arrive at the same answer. That is what makes the result trustworthy: thousands of independent machines checked the work and agreed.

Now suppose a contract could call an exchange's API. Two nodes running the same transaction a second apart get different prices. They compute different results, disagree about the new state, and the network stops being able to agree on anything. ethereum.org names this directly, noting the price "is likely to change frequently (not to mention that the API could get deprecated or hacked), meaning nodes executing the same contract code would arrive at different results."

So the price has to already be on-chain before your transaction runs. Somebody has to have put it there, in a transaction of their own, that every node can replay identically.

An oracle is not a way for a contract to see out. It is a person or a system writing a number into storage, which your contract then trusts.

Push and pull

Two shapes, and the difference is who pays and when.

A push oracle writes prices on-chain in advance, on its own schedule. Your contract reads a value that is already sitting there. Chainlink's classic Data Feeds work this way.

A pull oracle keeps prices off-chain, signed, and lets whoever needs one submit it as part of their own transaction. Pyth works this way: prices are aggregated on Pythnet every 400 milliseconds and only written on-chain when a consumer asks for one. Chainlink's documentation notes that pull-based oracles "operate more efficiently by retrieving data only when necessary."

Push costs somebody gas continuously whether or not anyone reads it. Pull costs the reader, and the reader is responsible for bringing a fresh price.

How a push feed actually updates

Chainlink feeds do not update every block, or on any fixed schedule. A new price is written when either of two conditions fires, whichever comes first:

  • the deviation threshold, when off-chain prices have moved more than a set percentage from the on-chain value
  • the heartbeat, when a set amount of time has passed since the last update

Between those triggers, the on-chain price is allowed to be wrong. That is not a bug, it is the design. Writing on every tick would cost more gas than the feed is worth.

For Ethereum mainnet ETH/USD, the deviation threshold is 0.5% and the heartbeat is 3,600 seconds. So the published price can legitimately sit half a percent away from the market for up to an hour.

Heartbeats, per feed

"Chainlink feeds have about a one-hour heartbeat" is the version in circulation. It is wrong for most feeds.

Chainlink publishes its Ethereum mainnet feed metadata as a JSON file. As of 17 August 2026 it lists 290 feeds, and the heartbeats break down like this:

output
20 feeds 3,600 s 1 hour 234 feeds 86,400 s 24 hours 7 feeds 3,024,000 s 35 days 29 feeds various 2h to 3d

234 of 290 feeds tolerate a full day between updates. Deviation thresholds span from 0.0000001% to 1000%, with 0.5% the most common at 100 feeds and 2% next at 65.

So "the Chainlink price" means very different things depending on which feed you picked. ETH/USD is tight. Most of the directory is not, and a contract reading a 24-hour-heartbeat feed during a fast move is reading history.

Never state a heartbeat without naming the feed and the chain.

What a consumer is supposed to check

Chainlink's latestRoundData() returns five values:

output
roundId, answer, startedAt, updatedAt, answeredInRound

The documentation is explicit about the obligation: your application "should track the latestTimestamp variable or use the updatedAt value from the latestRoundData() function to make sure that the latest answer is recent enough for your application to use it."

In other words, the feed does not tell you it is stale. You have to decide what stale means and check.

You will see answeredInRound >= roundId recommended as a staleness check. Chainlink's own API reference now documents answeredInRound as "Deprecated - Previously used when answers could take multiple rounds to be computed." Check updatedAt instead.

What the reference implementations actually check

Chainlink's own example consumer does not check. The sample contract on the Using Data Feeds page calls latestRoundData() and destructures only the answer. The other four return values, including updatedAt, are left as commented-out blanks. The file carries a header warning that it is an example and not production-ready, which is fair, and it is also the first code most integrators copy.

Aave v3's price oracle does not check either. In AaveOracle.sol, getAssetPrice calls source.latestAnswer() and returns the value if it is greater than zero, otherwise falling through to a fallback oracle. latestAnswer() returns only the price. There is no timestamp in the return value at all, so there is no freshness check to perform.

That is not a claim that Aave is broken. Their risk framework handles this at other layers, and the assets they list are the tight, high-frequency feeds. It is a demonstration that "use Chainlink" is not the same as "check the price is fresh," and that the second thing is left to the integrator on some very large protocols.

The circuit breaker

Chainlink aggregators carry minAnswer and maxAnswer parameters, often described as a price floor and ceiling that protect against absurd values.

The aggregator requires the median to sit inside the range, and if it does not, the transmission reverts. The feed does not clamp to the boundary. It stops updating and keeps returning the last in-range answer, which gets staler every block.

A live read of the ETH/USD aggregator at 0x7d4e742018fb52e48b08be73d041c18b21de6fb5 on 18 August 2026 shows minAnswer of 1 raw unit and maxAnswer of 2^176 − 1. Effectively unbounded. Whatever protection these parameters are assumed to give, on this feed today they give none.

Time-weighted prices, and what they cost to move

The other major approach is to derive a price from an on-chain market rather than import one.

A time-weighted average price takes the price a pool quoted over a window and averages it. Moving a spot price for one block is cheap: buy aggressively and the pool quotes whatever you pushed it to. Moving an average over many blocks means holding it there, and every block you hold it, arbitrageurs trade against you.

Uniswap's own numbers are the honest way to size this. Their 2022 analysis states that "a swap of 709 billion USDC manipulates the reference TWAP(time weighted average price) of WETH by 20%" for a two-block manipulation, and puts the cost of a three-block manipulation of the same size at $978 million.

Those numbers are not the same thing and the distinction matters. The 709 billion is the swap size you must push through. The 978 million is what it costs you.

TWAPs are manipulable. Uniswap's v2 whitepaper concedes that an entity producing consecutive blocks can do it, and their proof-of-stake analysis puts a validator with a 10% share at three consecutive blocks 181 times a month. Three other failure modes have nothing to do with economics: a pool with an observation cardinality of 1 makes any lookback revert, a thin pool makes the cost model cheap in absolute terms, and a long averaging window means the price is deliberately stale exactly when the market is moving fastest.

Securing the unseen

Four questions, answerable from published data, about any protocol you are trusting with money.

Which feed does it read, and what are that feed's actual heartbeat and deviation threshold? Does the contract check updatedAt, and against what limit? If the feed stops updating, what does the protocol do, and can that state persist? And if it derives a price from a pool instead, how deep is that pool and how long is the window?

An oracle is a trusted input. Aggregation across many nodes, signatures, deviation thresholds and time-weighting all raise the cost of lying, and none of them removes the trust. The question is never whether a price feed can be wrong. It is how wrong, for how long, and what your contract does about it.

Learn more at 0xhades.io/research

Sources

14 references · 9 primary
  1. ethereum.org, Oracles Why a contract cannot fetch external data itself, the determinism requirement, and the immediate-read, publish-subscribe and request-response oracle patterns. Accessed August 2026. ethereum.org primary
  2. Chainlink, Decentralized Data Model An aggregation round begins on whichever of the deviation threshold or heartbeat threshold fires first. docs.chain.link primary
  3. Chainlink, Data Feeds overview The instruction that applications should track latestTimestamp or updatedAt to confirm an answer is recent enough to use. docs.chain.link primary
  4. Chainlink, Data Feeds API Reference The five return values of latestRoundData, and answeredInRound documented as deprecated. docs.chain.link primary
  5. Chainlink, Using Data Feeds The sample consumer that destructures only the answer and leaves updatedAt commented out. docs.chain.link primary
  6. Chainlink reference data directory, Ethereum mainnet feeds The 290 feed entries with per-feed heartbeat and threshold, read 17 August 2026. ETH/USD at 3600s and 0.5%, USDC/USD at 82800s and 0.25%, and the distribution of heartbeats and thresholds quoted in the article. reference-data-directory.vercel.app
  7. smartcontractkit/libocr, OffchainAggregator.sol The minAnswer and maxAnswer requirement that reverts the transmission rather than clamping the price. raw.githubusercontent.com
  8. Chainlink, Offchain Reporting Aggregation performed off-chain by a leader and quorum of followers, with a single transaction posted on-chain. docs.chain.link primary
  9. Pyth Network, pull updates Aggregation on Pythnet every 400 milliseconds and the on-chain price written only when a consumer requests it. docs.pyth.network
  10. aave-dao/aave-v3-origin, AaveOracle.sol getAssetPrice calling latestAnswer and returning any price above zero, with no freshness check and no timestamp available to check. raw.githubusercontent.com
  11. Uniswap v3 TWAP Oracles in Proof of Stake The 709 billion USDC swap size for a 20% two-block manipulation, the $978 million cost of a three-block manipulation, and the frequency with which a 10% validator produces three consecutive blocks. blog.uniswap.org primary
  12. Uniswap v2 Core whitepaper The acknowledgement that an entity producing consecutive blocks can manipulate a time-weighted price. app.uniswap.org primary
  13. Uniswap v3 documentation, Price Oracles Observation cardinality and the requirement to grow it before a lookback window is usable. developers.uniswap.org primary
  14. makerdao/osm, osm.sol A delayed price feed that holds a value for a fixed period before it becomes current, as an example of deliberate staleness used defensively. raw.githubusercontent.com