Blockwright
September 22, 2026·11 min

The Chain Is Intact, the Money Is Gone: Oracle Attacks and Reading Prices Safely in Solidity

In 2026 protocols have lost more to price manipulation than in the previous three years: 32 attacks against 12 in all of 2025. And not a single blockchain was hacked — attackers break not the chain, but where the contract learns its price.

In early September, the Tectonic protocol on Cronos lost about 75 million dollars. The blockchain itself worked as normal — consensus didn't break, no private keys leaked, the contract code had no overflow. In a word — smooth sailing. The attacker simply made the contract believe a wrong price, and after that it honestly, as if that were how it should be, by all the rules, handed over other people's money.

This isn't a one-off, it's the trend of the year. According to TRM Labs, in 2026 protocols have been hit by 32 price-manipulation attacks against 12 in all of 2025, which, mind you, is nearly three times as many, and the year isn't even over yet. The share of such attacks among all crypto hacks grew from roughly one in seventeen in 2022 to one in eight now. Among the victims of recent weeks — Moonwell on Base ($8.7M), More Markets on Flow EVM ($410K), Cozy Finance on Optimism ($170K), Full Sail on Sui ($91K).

They all have one thing in common. What got broken wasn't the blockchain, but the way into it.

Why a contract has a weak spot at all

A smart contract lives in a deterministic cage. It has no fetch(), no clock, no source of randomness. That's by design, otherwise nodes could never agree on anything while executing the same code. Someone's milliseconds didn't match, someone fetched data from the central bank's website and it diverged, and so on and so forth. This is a fundamental property, not a blunder by its creators.

But every lending protocol contract wants to know what its collateral is worth at every given moment, and the exchange rate lives outside, in the old world, in web2. So this bridge between the worlds is called an Oracle: someone brings the data inside with an ordinary transaction, and from that moment it is deterministic for everyone.

And here is the paradox the whole thing was about. We built a system where you don't have to trust anyone, and put at its entrance a component you have to trust. That, in fact, is "the oracle problem". All the security of a protocol worth millions of dollars comes down to a single question. Where did this number, this rate, this data come from, and can it be influenced.

The first attack: the spot price from a pool

The most common way to learn a price "on-chain, with no middlemen" is to look into a DEX liquidity pool. It looks beautiful: data from the blockchain itself, no external services.

// DON'T DO THIS
function getPrice() public view returns (uint256) {
    (uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
    return (uint256(reserve1) * 1e18) / uint256(reserve0);
}

Three simple lines, reads like a reference book, nothing superfluous, but the problem is that this isn't "the token price" — it's the ratio of reserves in one particular pool right now, and the ratio of reserves can be changed by anyone who has money for a swap.

Next comes a mechanism that web2 doesn't have, the so-called flash loan. You can borrow millions with no collateral, but on one condition only: you have to return it in the same transaction, no sooner and no later, one transaction to receive and return the funds. If you don't return it, the transaction rolls back entirely, as if nothing had happened. In this situation the lender risks nothing, so anyone can borrow.

Now put two and two together:

  • 1Take out a large flash loan
  • 2Buy up one of the pool's tokens — the reserve ratio skews, the "price" shoots up
  • 3In the same block, poke the victim protocol, which reads getReserves() and sees the inflated price
  • 4Deposit the now-expensive token as collateral and take out a real loan against it
  • 5Repay the flash loan, walk away with the difference

That's all there is to it: one transaction — one attack, millions in your pocket. Arbitrageurs don't get a single block to put the price back, because the manipulation and its exploitation happen atomically (hello, atomicity). That's why the industry has a hard rule: a protocol that takes its spot price from a liquidity pool will be hacked. Not "might be" — will be.

Case study: Mango Markets, $110 million in ten minutes

A history lesson from October 2022. Avraham Eisenberg came to Mango Markets with 10 million dollars, which he had prudently split across two accounts. On one account he opened a short of 488 million MNGO, on the other he simply bought them up.

His goal wasn't to profit from the trade, but to paint the price. MNGO is an illiquid token, and the volume was enough to push its price up by roughly 2000% relative to its ten-day average. The protocol valued the collateral at this painted price, saw gigantic backing on the account and, without a second thought, lent against it an amount that wiped out the protocol. The result: over 110 million dollars.

Then came the most interesting part. Eisenberg publicly called it "a highly profitable trading strategy" and offered to return part of it, keeping tens of millions as a "bug bounty". His argument was dead simple: I didn't hack the code, I made trades the rules allowed. The CFTC, the SEC and the US Department of Justice didn't agree with that reading, and he was found guilty of fraud and market manipulation.

The technical moral is this: the code worked exactly as written. Not a single line was violated, because the vulnerability wasn't in the lending logic, but in the fact that the source of truth about the price was something that could be influenced with money.

TWAP: it helps, but it doesn't save you

The first reflex that kicks in for the average engineer is to average. A Time-Weighted Average Price takes the price not "now" but averaged over a period of time, say half an hour or an hour. Then a single-transaction flash loan stops working, because to move a 30-minute average you have to hold the skewed price for at least those 30 minutes, fending off arbitrageurs and everyone else who spotted a chance to make money. That's no longer free, it's expensive and risky.

But TWAP sends two bills for that protection at once.

The first bill: it lags by definition, because in a real market crash TWAP keeps showing yesterday's world for several more minutes, and the protocol makes decisions on stale data in the meantime, and that, mind you, means liquidations that didn't fire, or fired, but on the wrong accounts and in the wrong places.

The second bill: TWAP can only protect you from a cheap attack, while an expensive attack is out of its league. If a protocol holds hundreds of millions and an attack on an illiquid pool costs a few million, then the economics are still on the attacker's side. That's exactly why the list of 2026 victims has so many protocols on young networks. They simply have thin liquidity, and moving the price is cheap.

The right way — and the second trap

The industrial answer is a decentralized oracle network. With Chainlink, the price is brought in not by one node but by dozens of independent ones, each taking data from several exchanges; the result is aggregated and published when the price has moved beyond a set threshold (deviation threshold) or a time limit has passed (heartbeat). To fake that, you have to compromise a majority of the nodes, and that is far more expensive than what you could steal.

Reading it looks like this:

(, int256 answer, , , ) = feed.latestRoundData();
uint256 price = uint256(answer);

And this is where nine out of ten tutorials stop — a pity. Because you can't write it like this either.

On May 13, 2022, the price of LUNA was plunging into the abyss like an e-scooter rider falling into hell. The Chainlink feed had a hard-coded lower bound minAnswer at about $0.10, insurance, so to speak, against "garbage" values. When the market went below it, the feed hit that bound and was paused due to unprecedented volatility.

And from there, it's arithmetic. The oracle showed ~$0.107 while the market showed ~$0.01. A tenfold difference, and it held.

Attackers deposited 230 million LUNA into Venus Protocol at the oracle price, which is roughly more than 24 million dollars of "collateral", while the real price of all this madness was about 2 million. They borrowed ~13.5 million in assets against that collateral and left. Venus lost about 11 million dollars. Blizz Finance, which worked the same way, lost over 8.3 million and shut down, because it had been drained dry.

The oracle wasn't hacked. It worked exactly to spec. What got hacked were the protocols that didn't check what they received.

Reading a price safely

Let's put it all together into one working example. This isn't "prettier", it's the minimum that is enough.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface AggregatorV3Interface {
    function latestRoundData()
        external
        view
        returns (
            uint80 roundId,
            int256 answer,
            uint256 startedAt,
            uint256 updatedAt,
            uint80 answeredInRound
        );
    function decimals() external view returns (uint8);
}

contract PriceReader {
    error BadPrice(int256 answer);
    error StalePrice(uint256 updatedAt, uint256 nowTs);
    error PriceOutOfBounds(uint256 price);

    AggregatorV3Interface public immutable feed;

    /// the feed's heartbeat plus a margin. Taken from the docs of THIS PARTICULAR feed,
    /// not copied from someone else's project: it differs between pairs.
    uint256 public immutable maxDelay;

    /// the bounds of common sense for this asset
    uint256 public immutable minPrice;
    uint256 public immutable maxPrice;

    constructor(address feed_, uint256 maxDelay_, uint256 minPrice_, uint256 maxPrice_) {
        feed = AggregatorV3Interface(feed_);
        maxDelay = maxDelay_;
        minPrice = minPrice_;
        maxPrice = maxPrice_;
    }

    function getPrice() public view returns (uint256) {
        (, int256 answer, , uint256 updatedAt, ) = feed.latestRoundData();

        // 1. the price is positive at all
        if (answer <= 0) revert BadPrice(answer);

        // 2. the data is fresh: the feed isn't stuck or paused
        if (updatedAt == 0 || block.timestamp - updatedAt > maxDelay) {
            revert StalePrice(updatedAt, block.timestamp);
        }

        uint256 price = uint256(answer);

        // 3. the price is within common sense — insurance against the feed
        //    hitting its own minAnswer/maxAnswer (the LUNA case)
        if (price < minPrice || price > maxPrice) revert PriceOutOfBounds(price);

        return price;
    }
}

Three checks, each closing off a specific way to lose money.

The freshness check — against a stuck or paused feed. The value of maxDelay is taken from the heartbeat of the specific pair: ETH/USD has one, something exotic has a completely different one, and plugging in a round number "by eye" means either false alarms or a hole you could drive a truck through.

Bounds — that very LUNA lesson. If the feed has hit its internal minAnswer, formally it returns a valid number, and the only way to notice is to compare it against the range in which the price makes sense at all. There's an unpleasant trade-off here: bounds that are too narrow become a denial of service themselves during a sharp market move, so bounds are set wide, at "physically meaningful values".

A separate note on L2. If the contract lives on Base, Optimism or Arbitrum, then be sure to also read the sequencer uptime feed. When the sequencer goes down and comes back up, prices arrive in a batch, and at that moment liquidations run on data from the past. Chainlink provides a separate feed for "is the sequencer alive and how long since it came back up", a very simple rule that will save you: after recovery, wait out a grace period and only then make decisions based on the price. Tellingly, two of the recent 2026 incidents happened precisely on L2.

Checklist: auditing your own code

Go through your project right now.

  • Is the price taken from getReserves() or anything "in the pool right now"? That's not an oracle, that's a hole
  • Is the result of latestRoundData() used without checking answer > 0?
  • Is freshness checked via updatedAt, and is maxDelay taken from the docs of this particular feed?
  • Are there price bounds for the case where the feed hits minAnswer/maxAnswer?
  • Is the contract on L2? Is the sequencer uptime feed being read?
  • One price source for the whole protocol? Think about a second, independent one, and about the divergence between them as an emergency brake
  • Can the price be moved and immediately exploited within a single transaction? If yes, the attack costs exactly one flash loan
  • What happens if the oracle reverts? Does the protocol halt, or does it take the last known value just in case? The second is more dangerous than it looks

What follows from all this

A funny paradox of the industry: we spend enormous effort making computation trustless, and then reduce all security to a single number that arrived from outside. The blockchain really is nearly impossible to hack, so that's not what gets hacked.

Thirty-two attacks in less than a year and more than a billion in total DeFi losses for 2026 say this isn't something exotic for auditors, but everyday routine. And almost every one of them boils down to a single sentence. The contract trusted a number it didn't check.

If you're writing a contract that touches money, write one question down on a piece of paper: where do I learn the price, and how much does it cost to influence it. Until you have a numeric answer to the second half, the protocol isn't ready.

This is Wright. No bugs, no hacks — break a leg, friend!