Attack classes
Oracle Manipulation Attack: Spot, TWAP, Chainlink and Pull Oracles Compared
An oracle manipulation attack rarely breaks the oracle. It moves the venue the oracle reports, or reads a price the protocol should never have trusted, then borrows against the result. This comparison sizes spot reads, Uniswap TWAPs, Chainlink push feeds and pull oracles by what an attacker has to control, and carries our own survey of every Chainlink feed parameter published on four networks.

Key facts
- Feeds that may sit unchanged for a day
- 220 of 247 plausible Chainlink feeds on Ethereum mainnet carry a heartbeat of 86,400 seconds or more, and only 20 are obliged to update at least hourly. Our survey of the reference data directory, September 4, 2026
- How wide the price may drift
- The modal deviation threshold is 0.5 percent on all four networks surveyed, but 67 of the 247 Ethereum mainnet feeds tolerate 2 percent or more before a round fires
- What a TWAP costs to beat
- Moving the Uniswap USDC/WETH 5 basis point pool 20 percent over two blocks took about 709 billion USDC, and cost falls exponentially as an attacker controls more consecutive blocks
- Chainlink Streams TWAP entries
- Twenty entries over ten USD pairs in 30 second and 60 second windows, present in the Arbitrum directory file as Data Streams verifier contracts rather than push aggregators when it was pulled on September 4, 2026
- The case that is not an oracle bug
- Mango Markets was genuine market price manipulation, MNGO spot from 0.03 to 0.91 dollars, with unrealized profit posted as collateral. The feeds reported what the market said
Manipulation cost model per feed design
An oracle manipulation attack is an exploit that changes what a protocol believes an asset is worth, then extracts value at that wrong price. Nothing in the oracle contract has to be broken for it to work. The pattern is arithmetic. Move a venue with borrowed capital, get the protocol to read the moved venue, then borrow, mint or liquidate against the number it now holds. That price describes nothing outside the transaction that produced it.
Feed designs differ in one variable that outweighs the rest: what an attacker must control, and for how long. Reading an AMM pool's ratio of reserves needs one transaction. A time weighted average price (TWAP) demands that the dislocated price survive every block in the window. Costs on those rungs differ by orders of magnitude, and that difference is the whole design argument.
Uniswap Labs published the arithmetic for its own pools in research written with Euler, and the table below carries the two figures it turns on. Note the direction. Cost keeps falling as the attacker holds more consecutive blocks, an exponential decrease, and the authors still call the three block number prohibitively expensive.
Depth sets the price more than the window does. Adding a wide range liquidity position of about 1,240,000 dollars to USDC/WETH raised the two block manipulation cost by roughly 360 billion dollars. Underneath is a tick count: how many ticks an attacker must cross to move the reference price by the target percentage, which is why a thin pool and a deep pool behave nothing alike under the same formula.
| Feed design | What the attacker must control | Measured cost | Source |
|---|---|---|---|
| Spot pool price | One transaction | Not quantified on this page | None |
| LP token price | One transaction and one callback | Not quantified on this page | None |
| TWAP over n blocks | n consecutive blocks at the dislocated price | About 709 billion USDC over two blocks, about 978 million dollars over three | Uniswap Labs with Euler, USDC/WETH 5 basis point pool moved 20 percent |
| Push feed | The venues the oracle network samples | Not quantified on this page | None |
| Pull feed | A signed report inside the seconds it stays valid | Not quantified on this page | None |
Spot and LP prices
Using a pool's spot rate as an oracle means dividing one side of the pool by the other and calling the quotient a price. A flash loan rewrites that quotient for one transaction at no capital risk. That is why the bottom rung keeps producing losses year after year, long after the mechanism stopped being novel. Our note on flash loan attack patterns covers the borrowing side of the same move.
Liquidity provider (LP) token
pricing has the same defect in a less obvious wrapper. A naive
implementation reads live reserves and the token supply, so a flash loan
sandwich can skew the reserves and fake the underlying position. Alpha
Venture DAO published the standard correction: value the LP token as
2 * sqrt(r0 * r1) * sqrt(p0 * p1) / totalSupply, where p0 and
p1 come from an independent feed. That formula infers fair reserves from a fair price ratio rather than
trusting whatever getReserves() returns mid transaction. Warp
Finance lost about 7.8 million dollars on
to the naive version.
Reserves are not the only manipulable input. ChainSecurity's post mortem on
Curve showed that get_virtual_price carries no reentrancy
guard, so it can be called from inside a remove_liquidity
callback where the pool's invariant is temporarily inconsistent with the LP
supply. That left MakerDAO, Enzyme, Abracadabra, TribeDAO and Opyn exposed,
more than a hundred million dollars at risk by the firm's own count, with
the ETH/stETH pool's LP token price manipulable by a factor of two.
Disclosure went out on
. The fix is unglamorous:
call withdraw_admin_fees before reading, which trips the pool's
reentrancy lock and reverts a mid callback read. One line. Firms that publish work of
this kind are worth reading directly, and our
ChainSecurity profile
lists where theirs lives.
Uniswap TWAP
A Uniswap v3 pool stores a cumulative tick accumulator rather than a price, and the consumer computes a mean over whatever window it asks for. Two consequences follow. Window length is a consumer decision, not a pool property, so two protocols reading the same pool can hold very different risk. Offchain venues usually quote a volume weighted average price (VWAP), which answers a different question from the time weighted mean an onchain accumulator produces, and the two are not interchangeable in a liquidation formula.
Cost falls as an attacker controls more consecutive blocks for a fixed displacement. It does not rise with the window. That matters because proof of stake made consecutive block control schedulable: proposer schedules are known ahead of time, and a validator holding a 1 percent share should expect three consecutive blocks about 0.19 times per month. A TWAP is a bet on validator concentration and pool depth together, and only one of those two is under the protocol's control.
Depth is the part teams underestimate. Inverse Finance lost about 15.6 million dollars on , an entry the rekt leaderboard files against a TWAP taken over a thin INV pool. A window that costs 978 million dollars to beat on USDC/WETH costs a rounding error on a pair with a few hundred thousand dollars of liquidity near the current tick. Sizing that number is the engineering task. Picking 30 minutes because another protocol did is not. Both habits turn up in integration code.
Chainlink push feeds and the Streams TWAP entries
A Chainlink push aggregator updates on whichever of two conditions fires first. Deviation threshold means a new aggregation round starts when nodes see the offchain value move by more than the configured percentage away from the onchain value. Heartbeat means a round starts after a fixed amount of time since the last update regardless of price. Between those two events the onchain answer does not change. That gap is the surface a protocol inherits the moment it reads the feed. Its width is published rather than secret, which makes it one of the few risks here a team can size before writing code.
| Feed | Heartbeat (s) | Deviation (%) | Sibling disagreement |
|---|---|---|---|
| ETH / USD | 3,600 | 0.5 | no |
| BTC / USD | 3,600 | 0.5 | no |
| USDC / USD | 82,800 | 0.25 | yes, 82,800 and 86,400 |
| USDT / USD | 86,400 | 0.25 | no |
| DAI / USD | 3,600 | 0.25 | no |
| STETH / ETH | 86,400 | 0.5 | no |
| LINK / USD | 3,600 | 1 | yes, 1 and 0.5 |
| AAVE / USD | 3,600 | 1 | no |
| UNI / USD | 3,600 | 1 | no |
| EUR / USD | 86,400 | 0.5 | yes, 0.5 and 0.15 |
| XAU / USD | 86,400 | 0.3 | no |
Twenty Chainlink Streams TWAP entries were present in the Arbitrum directory file when we pulled it on . They are not push aggregators. No push aggregator sits behind them, so there is no onchain answer to poll, and the averaging window is 30 or 60 seconds rather than the tens of minutes an onchain accumulator usually implies.
- Entries
- 20
- Pairs
- Ten, against the US dollar: BNB, BTC, DOGE, ETH, HYPE, LINK, SOL, TRX, XRP and ZEC
- Windows
- 30 seconds and 60 seconds, ten of each
- Contract type
verifier, at a Streams service level- Directory file
- The Arbitrum reference data directory file
What we measured across four networks
Comparisons of oracle designs almost never carry a parameter. So we ran one.
Method
- Source
- Chainlink's reference data directory, the same source that backs the feed listing pages. All four files answered HTTP 200.
- Retrieved
- Networks
- Ethereum mainnet, Arbitrum, Base and Optimism, with raw row counts of 286, 1,252, 182 and 158.
- Inclusion rule
- Rows with a deviation threshold below 0.01 percent or above 10 percent, or a heartbeat under one second, were excluded. Those are mostly net asset value (NAV) and proof of reserve entries whose thresholds sit near zero. Arbitrum loses most of its file to the rule because it carries hundreds of equity and tokenized equity rows.
- Denominator
- The plausible count for each network, never the raw one.
Results
| Network | Raw rows | Excluded rows | Plausible | Heartbeat 86,400 s or more | Heartbeat 3,600 s or less | Modal deviation | Deviation 2% or more |
|---|---|---|---|---|---|---|---|
| Ethereum mainnet | 286 | 39 | 247 | 220 (89%) | 20 | 0.5% | 67 |
| Arbitrum | 1,252 | 1,006 | 246 | 215 (87%) | 28 | 0.5% | 4 |
| Base | 182 | 22 | 160 | 139 (87%) | 20 | 0.5% | 2 |
| Optimism | 158 | 12 | 146 | 118 (81%) | 28 | 0.5% | 1 |
Read the Ethereum column slowly. Of 247 plausible feeds, 220 can legitimately sit unchanged for 24 hours or longer, and only 20 are obliged to update at least hourly. Between updates the price is allowed to drift by the deviation threshold, which is 0.5 percent on the modal feed and 2 percent or worse on 67 of them. A lending market reading that during a fast move is not reading a stale feed by accident. The feed is behaving exactly as configured, and the protocol agreed to a price which may lag the market until a deviation round fires, so collateral factors, liquidation bonuses and close factors all have to be set against that width. Read the table across and a second pattern shows up: the L2s carry far fewer wide deviation feeds, 4 on Arbitrum, 2 on Base and 1 on Optimism against 67 on Ethereum, so an L2 deployment inherits a narrower price band and a sequencer risk the L1 does not have.
Limits
- Same named feeds disagree in the directory. USDC/USD appears with both 82,800 and 86,400 second heartbeats, LINK/USD with both 1 and 0.5 percent deviation and EUR/USD with both 0.5 and 0.15 percent. The tables here take the entry the survey matched and flag the conflict rather than averaging it away.
- Our spot check against the public feed pages at data.chain.link answered HTTP 429 on both attempts, so these values are confirmed against the directory and not page by page.
-
Feed sets change. The survey script and its output are kept in the site
repository as
seo/research/chainlink-feed-survey.pyandseo/research/chainlink-feed-survey-2026-09-04.json, neither of which is published on the site, and every figure above carries its retrieval date of .
Pull oracles
Pull designs invert the delivery model. Instead of an aggregator writing prices onchain on a schedule, the consumer retrieves a signed report and verifies it onchain at the moment it is needed. Chainlink Data Streams describes itself that way, advertises sub second data resolution for latency sensitive use cases, and carries mid prices alongside Liquidity Weighted Bid and Ask. Verification confirms that the decentralized oracle network agreed on and signed the data, which is where the trust assumption sits.
Pyth publishes the sharpest guidance on how to consume such a price safely.
Every Pyth price carries a confidence interval intended to achieve 95
percent coverage, and the documented practice for collateral is to value the
position at the lower bound of that interval, so an attacker pushing the
price down gains less than the headline suggests. Its worked example values
bitcoin at 49,000 dollars when originating a loan. Staleness is handled by
default in the SDKs, and
getPriceNoOlderThan() lets the caller state the maximum age it
will accept; latency sensitive protocols are advised to cut that threshold
to a few seconds. RedStone and Chronicle occupy the same design category and
are worth evaluating alongside, though their parameters are outside what we
checked here.
Freshness on demand comes with its own bill. Somebody has to submit the report, the transaction that does so is visible before it lands, and the protocol has to decide what happens when nobody submits one. Pull removes the heartbeat problem and hands back a liveness problem.
| Protocol | Recommended feed design | The parameter that decides it | The disqualifier |
|---|---|---|---|
| Lending market | Push feed, heartbeat and deviation width chosen rather than inherited | Heartbeat and deviation width set against the liquidation math | A configured drift wider than the liquidation bonus |
| Perpetuals venue | Pull feed with a staleness bound of a few seconds | Pull freshness against the per trade cost of carrying a report | A heartbeat measured in hours |
| Slow moving vault | Push feed | Heartbeat and deviation width against the liquidation math | A per trade verification cost for freshness nobody uses |
| Stablecoin peg | TWAP over a deep pool, or a push feed | TWAP window against the cost of controlling consecutive blocks | A pool thin enough that the window buys nothing |
Auditor checklist
Every item below has failed in production somewhere, and each is checkable by reading the integration contract rather than the oracle. Run it before an engagement starts. What a finished report should say about each is covered in our guide to reading an audit report.
- Sources
-
Is more than one provider wired in, and is the fallback path real? Aave's
oracle sets per asset price sources through
setAssetSourcesand supports a configurable fallback throughsetFallbackOracle, which is only useful if somebody has decided when the fallback wins. - Aggregation
- Does the protocol read a network aggregated price or an AMM quote? A single pool read is the cheapest rung of the ladder no matter how the variable is named.
- Heartbeat
- Does the freshness threshold in code match the feed's configured heartbeat? A one hour bound applied to a feed whose heartbeat is 86,400 seconds will revert on the normal case, and 220 of 247 plausible Ethereum mainnet feeds are in that category.
- Deviation
- Can the protocol survive a price that is legitimately off by the threshold? Set that against 0.5 percent on the modal feed and 2 percent or more on 67 Ethereum mainnet feeds.
- Staleness
-
Is
updatedAtcompared against block time, and is the round identifier handled rather than discarded? Ignoring the timestamp is the cheapest of these to get wrong: the code compiles, the tests pass and the bound never fires. - Sequencer uptime
-
On a layer two (L2) rollup, is the
Chainlink sequencer uptime feed read before the price? It answers
0when the sequencer is up and1when it is down, and the documented pattern pairs it with a grace period after recovery to prevent mass liquidations. Chainlink's own example uses 3,600 seconds. Aave takes a comparable route through itsPriceOracleSentinel, which introduces a grace period for liquidations and disables borrowing under specific circumstances, with the duration set in seconds throughsetGracePeriod. - Circuit breakers
-
Is there a bound on how far one update may move the recorded price, a
minimum interval between updates and a way to halt? Makina's oracle had
none of those: no access control on
updateTotalAum(), no time weighted pricing, no delay between updates, no comparison against previous values and no breaker for abnormal movements. Design notes for that control live in our piece on emergency pauses. - Verification
- Fork past the heartbeat, re-run with a future timestamp and a backwards round identifier, and expect the staleness path to trigger. All three are cheap to simulate. None is covered by a happy path test.
Two habits make the list stick. Write the oracle assumptions as invariants and fuzz them, since property based testing finds the combination of a stale answer and a paused sequencer faster than a reviewer reading top down. Then watch the same invariants in production with runtime monitoring, because a feed's parameters can change after the report is signed. Teams sizing an engagement around a price dependent system can compare firms through the alliance member directory or scope one directly in the audit builder, and our lending protocol audit guide walks the collateral side in more depth.
MEV and sandwich exposure of price paths
Maximal extractable value (MEV) is the reason a price path has to be evaluated as an ordering problem and not only as a data problem. Under proof of stake the proposer schedule is public, so holding several blocks in a row stopped being a matter of luck, and that single change is what forced the TWAP cost models to be rewritten. Any design whose safety argument rests on an attacker being unable to influence consecutive blocks now needs the validator concentration numbers written down beside it.
Capital stopped being a constraint earlier than that. Makina's attacker borrowed 160.59 million USDC from Morpho and 119.41 million from Aave V2, 280 million dollars inside one transaction, dumped most of it into a stablecoin pool to distort the spot rate, then called the permissionless accounting function mid transaction. Nothing there required the attacker to own anything at the start of the block or to hold anything at the end of it. Flash loans convert an oracle weakness into an execution problem. Execution is cheap.
Mango Markets is the case most worth stating precisely, because it is routinely described as an oracle failure and it was not one. Rekt's account is explicit that this was genuine price manipulation: the attacker spiked the MNGO spot price from 0.03 to 0.91 dollars across venues using two accounts and self trades, and the unrealized profit on the perpetual position became collateral. Every oracle in that path reported what the market said. A protocol that reads a correct price from a market thin enough to be bought still has an oracle problem, which is why thin market listings, not feed selection, is often the finding that should have been written. Firms that publish this kind of reasoning rather than a badge are the ones worth shortlisting, and our ranking of leading security companies is a starting point.
Pull oracles deserve one closing caution. A design where the price update and the action ride in the same transaction removes the lag, but it puts the whole path in the mempool where its ordering can be observed and bid against. Neither model escapes MEV. Push feeds concentrate the exposure at the update, pull feeds concentrate it at the consumer, and the job is to know which one the protocol has chosen rather than to hope the oracle settles the question.
Incidents referenced on this page
| Incident | Date | Loss | Feed design attacked | Mechanism |
|---|---|---|---|---|
| Warp Finance | About 7.8 million dollars | Naive LP token price from live reserves | Flash loan sandwich skewed the reserves | |
| Curve read only reentrancy | , disclosure | More than a hundred million dollars at risk | LP price from get_virtual_price |
Read inside a remove_liquidity callback, invariant inconsistent with LP supply |
| Inverse Finance | About 15.6 million dollars | TWAP over a thin INV pool | Too little depth near the tick for the window to cost anything | |
| Makina | Not stated on this page | Not stated on this page | Permissionless AUM accounting with no time weighted pricing | 280 million dollars of flash loans dumped into a stablecoin pool, then updateTotalAum() called mid transaction |
| Mango Markets | Not stated on this page | Not stated on this page | Correct feeds over a thin market | MNGO spot driven from 0.03 to 0.91 dollars with two accounts and self trades, unrealized profit posted as collateral |
Frequently asked questions
How do I test staleness handling in a fork test?
Fork at a block where the feed is healthy, push block time past the feed's configured heartbeat without submitting an update, then call every entry point that touches a price. Borrow, liquidate and redeem should behave the way the specification says, which usually means reverting rather than transacting on an answer older than the bound. Repeat the run with the aggregator mocked to return a timestamp in the future and a round identifier that moves backwards. That is the Verification item in the auditor checklist above, and it is the item most integrations skip.
Which oracle design should a lending market pick?
A push feed, with the heartbeat and the deviation width chosen against the liquidation math rather than inherited from whatever the feed ships with. The width the price is allowed to drift between rounds has to sit inside the liquidation bonus, so a feed whose configured drift is wider than that bonus is disqualified before the integration is written. On a layer two, read the sequencer uptime feed before the price and hold a grace period after recovery. When the asset has no push feed at all, the honest choices are a pull feed with a staleness bound of a few seconds plus a keeper you fund, or not listing the asset; a TWAP over a thin pool is not a third choice.
Who pays for a pull oracle update?
Whoever sends the transaction that carries the signed report, which in practice means the user taking the action or a keeper the protocol funds. That shifts cost from a continuous onchain publishing budget to a per interaction one, and it introduces a liveness question that push feeds do not have: what the protocol does when no report arrives. The Pyth best practices page we checked publishes no fee figure, so measure the verification cost on your target chain instead of assuming it is negligible.
Does reading two independent oracles remove the risk?
Only if somebody writes the disagreement rule. Two sources protect against one provider failing, but a fallback that engages silently means the protocol quietly inherits the weaker source at the worst moment, and a median of two is just an average of a good price and a bad one. Decide in advance how far apart the sources may drift before the protocol stops transacting, make that bound a parameter an auditor can read, and log the switch so monitoring sees it happen.