Oracles: Chainlink, Pyth, Redstone, and TWAPs
Oracles are the bridge between on-chain code and the off-chain world. Almost every DeFi protocol depends on at least one. Almost every catastrophic DeFi exploit involves an oracle bug — directly (a manipulated price) or indirectly (a stale price, a missing deviation check, a fallback that returns wrong data).
This section covers the major oracle architectures, their failure modes, and the audit posture for any contract that consumes an oracle.
Oracle Architectures
Push Oracles (Chainlink Classic)
Oracle nodes monitor off-chain prices and push updates to on-chain aggregator contracts when thresholds are met (price deviation, time interval). The consumer reads the current price from the aggregator at any time.
Strengths:
- Familiar interface (
latestRoundData); easy integration. - Operates without consumer intervention.
- Established on virtually every chain.
Weaknesses:
- Price can be stale between updates.
- Update timing is observable; sandwichable.
- Per-feed economics (some feeds update infrequently; others have wider deviation thresholds).
- A consumer can't request a fresh price on demand.
Pull Oracles (Pyth, Chainlink Data Streams, Redstone)
Oracle nodes maintain off-chain signed prices that change continuously. The consumer fetches the latest signed price and submits it as part of the transaction that needs it. The on-chain verifier checks the signature and uses the price.
Strengths:
- Always-fresh price at execution time.
- Atomic price + action (no sandwich window).
- Cheaper to maintain on-chain (no per-update gas spent by the protocol unless used).
- High-frequency feeds available (sub-second on some).
Weaknesses:
- Requires off-chain fetching by the user or relayer.
- Signature verification adds gas cost per transaction.
- Failure modes depend on the relay infrastructure (e.g., Pyth's Wormhole relayer being unavailable).
TWAP Oracles (Uniswap V2 / V3 / Curve EMA)
The on-chain DEX itself exposes a moving-average price computed from its swap history. The protocol reads it directly from the DEX.
Strengths:
- No external dependency — the price is on-chain by construction.
- Resistant to single-block manipulation (manipulation costs scale with window length).
- Cheap to read.
Weaknesses:
- TWAP smoothing means the price lags during volatility.
- Manipulation cost scales with window × liquidity; for low-liquidity pools or short windows, manipulation is cheap.
- Requires sufficient observation cardinality (V3) to actually be a meaningful TWAP.
- Vulnerable to pool draining (the attacker removes their LP, leaving low liquidity, and manipulates the price for a fraction of the cost).
Custom / Hybrid
Some protocols build oracle systems specifically for their use case: a redundant aggregation of multiple sources, a TWAP with deviation circuit breakers, a "pessimistic" oracle (use the worse of multiple sources for safety). These are bespoke and warrant extra scrutiny.
Recurring Oracle Findings
1. Missing Staleness Check
The Chainlink-style consumer pattern, done wrong:
(, int256 answer, , ,) = oracle.latestRoundData();
require(answer > 0, "negative price");
uint256 price = uint256(answer); // ❌ no staleness check
Done right:
(uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = oracle.latestRoundData();
require(answer > 0, "invalid price");
require(updatedAt > 0, "round incomplete");
require(block.timestamp - updatedAt < MAX_STALENESS, "stale price");
require(answeredInRound >= roundId, "stale round");
uint256 price = uint256(answer);
The MAX_STALENESS should be tuned per-feed — Chainlink's "heartbeat" tells you the expected update interval; the staleness threshold should be larger than the heartbeat but small enough that the protocol doesn't use truly outdated data.
2. Missing Deviation / Sanity Check
Even with a fresh oracle, a single price source can be wrong (oracle bug, node compromise, market dislocation). Mitigations:
- Bounded acceptable price range: reject prices that deviate too far from a prior value.
- Multi-source aggregation: require at least N sources agree within a deviation threshold.
- Circuit breakers: pause the protocol when prices move faster than configured (e.g., > 10% in a single block).
These are particularly important for low-liquidity assets and during periods of high volatility.
3. Pyth-Specific: Confidence Interval Ignored
Pyth's price feeds include a confidence value — the standard deviation of the price estimate. A protocol that uses the price without considering confidence is vulnerable during low-confidence periods (e.g., immediately after a CEX outage).
PythStructs.Price memory p = pyth.getPriceNoOlderThan(feedId, MAX_AGE);
require(p.conf * MAX_CONF_RATIO < p.price, "low confidence");
// then use p.price
The 2024 Multichain liquidation issue and several others had a "confidence ignored" component.
4. Decimals Mismatch
Different oracles report prices with different decimal scales:
- Chainlink ETH/USD: 8 decimals.
- Pyth feeds: variable, encoded in the response.
- Uniswap V3 TWAP: tick-encoded; needs conversion to a fixed-point ratio.
A consumer that hard-codes decimals for one source and silently uses another will be off by orders of magnitude. Often catastrophic.
5. Wrong Quote Currency
ETH/USD is not ETH/USDC. BTC/USD is not WBTC/ETH. Mismatches between what the protocol thinks it's pricing and what the feed actually provides are routine findings.
Audit posture: for every oracle read, verify the exact feed identifier; the exact base and quote; and that the consumer's math treats them correctly.
6. Single-Block Manipulability
The fundamental MEV-adjacent oracle vulnerability:
// In the same block:
// 1. Attacker flash-borrows N tokens
// 2. Swaps N tokens to inflate price in low-liquidity pool
// 3. Reads price from the pool — inflated
// 4. Performs an action that profits from inflated price (liquidation, mint, etc.)
// 5. Swaps back, restoring price
// 6. Repays flash loan
This is why spot AMM prices should never be used for value-bearing logic. TWAPs and external oracles mitigate; the audit should verify that every value-bearing price read is either:
- A push oracle with staleness check, or
- A pull oracle with confidence check, or
- A TWAP with sufficient window + liquidity, or
- An internal exchange rate not exposed to single-block manipulation.
7. Cross-Domain Replay (Pull Oracles)
A Pyth or Redstone price update is a signed message. The verifier must check that the message is intended for the consuming chain and the consuming domain. Pyth's price IDs are global, but Pyth verifiers should check the message was destined for the consumer's chain via the Wormhole emitter chain field.
A bug where any chain's Pyth update can be replayed on any other chain has appeared in custom Pyth integrations.
8. Oracle Update Front-Running
A push-oracle update is observable in the mempool before it's included on-chain. An attacker who sees a large price move incoming can:
- Open a position at the old price (long if the new price is higher), in the same block.
- Wait for the oracle update to land.
- Close the position immediately after, capturing the price difference.
Mitigations: pull oracles with atomic price + action; batch oracle updates with all dependent actions; or accept this as a known cost and limit position-opening velocity.
9. L2 Sequencer Downtime
L2s typically use Chainlink "Sequencer Uptime Feeds" alongside price feeds. If the sequencer is offline, the price feed may not update; consumers should check the uptime feed and refuse to operate (or operate in safe mode) when the sequencer is down.
Audit: any L2 deployment of an oracle-dependent protocol should include sequencer-uptime checks.
Audit Checklist for Oracle Consumers
For every oracle read in the contract:
- The feed ID / address is the correct feed for the correct asset pair.
- The decimals are handled correctly.
-
Staleness check:
block.timestamp - updatedAt < MAX_STALENESSwith sensibleMAX_STALENESS. -
Validity: price is positive, round complete (
answeredInRound >= roundId), confidence acceptable. - Deviation: the price is sanity-checked against expected ranges or alternative sources.
- Single-block manipulability: cannot read a price within a transaction that also manipulates that price.
- On L2: sequencer uptime feed checked.
- For pull oracles: signature verified, chain/domain checked, replay protection in place.
- Failure mode: what happens when the oracle returns 0, reverts, or is stale? Revert is preferred to "use last known good price" for most protocols.
A Particularly Worth-Quoting Pattern
For protocols where oracle failure is unacceptable (perps, lending), a multi-source oracle abstraction is common:
function getPrice(address asset) public view returns (uint256) {
uint256 chainlink = _readChainlink(asset); // checks staleness, validity
uint256 pyth = _readPyth(asset); // checks confidence, freshness
uint256 deviation = _absDelta(chainlink, pyth) * 1e18 / chainlink;
require(deviation < MAX_DEVIATION, "sources diverged");
return (chainlink + pyth) / 2;
}
This is more robust than any single source. The trade-off is gas cost and the operational burden of maintaining multiple integrations. For high-TVL protocols it's worth it; for smaller protocols, a single source with appropriate checks is fine.
Closing Note
If you remember nothing else from this section: no value-bearing price should be read from a manipulable spot source. Every audit finding involving "the protocol used the AMM's instantaneous reserves as the price" is the same finding repeated. It is, and remains, the single most-exploited bug class in DeFi. Stop using spot prices.