The Null That Ate the Tape: Empty Data Feeds Are Crypto's Most Expensive Bug
Hook
Over the past ninety days I have pulled the liquidation engines of fourteen lending markets on Arbitrum, Base, and Optimism. Eleven of them share the same defect.
Not a reentrancy hole. Not an access-control mistake. Not an unchecked delegatecall.
A missing require.
Here is the shape of it. Every one of those protocols calls Chainlink's latestRoundData(). Every one checks that answer > 0. Almost none of them check updatedAt.
latestRoundData() returns five values: roundId, answer, startedAt, updatedAt, answeredInRound. Four of them are metadata. Integrators treat the metadata as noise. So when the underlying feed stops advancing โ because the heartbeat lapsed, because the aggregator is mid-migration, because the L2 sequencer that orders the transaction is down โ answer does not become zero. It does not revert. It keeps returning the last number that was true.
A plausible number. From a block that is now forty minutes old.
Your protocol reads it as truth. Your liquidation bot reads it as truth. And in a bear market, where there is no new TVL arriving to socialize a bad debt, that single missing line of code is the difference between a position that gets closed at a fair price and a position that gets closed at a price that never existed.
Zero is loud. Zero reverts. Zero trips every alerting rule you own.
Stale is silent. Silence is what kills you.
Context
Why this matters now, and not in 2021.
Three things changed.
The migration. The majority of active lending TVL no longer sits on Ethereum L1. It sits on L2s โ Arbitrum, Base, OP Mainnet. L2s have sequencers. Sequencers are, today, single points of ordering failure operated by a small number of entities. Chainlink shipped a Sequencer Uptime Feed specifically so that protocols could ask whether the sequencer is up before trusting a price. The feed exists. The integrations mostly do not read it, or read it and hardcode the grace period to zero.
The collateral mix. In 2021, collateral was ETH and stablecoins. In 2026, it is LSTs, LRTs, tokenized treasuries, and a long tail of yield-bearing wrappers, each with its own feed, its own heartbeat, and its own minAnswer / maxAnswer circuit breakers. Every new feed is a new opportunity for the answer to be technically present and practically wrong.
The agents. There are now at least nine protocols in testnet or early mainnet that replace the human oracle operator with an LLM-based agent that reads unstructured sources โ exchange APIs, news wires, governance forums โ and posts a signed price. I have capital deployed in one of them. I will get to what I found.
But before any of that, you need to understand why this bug class is a Solidity problem and not just an oracle problem.
Core
Start with the language.
Solidity does not have null. It does not have undefined. It does not have None. Every storage slot is initialized to its type's zero value. A uint256 is 0. An address is 0x0. A bool is false. A string is empty.
This is not a bug. It is a design decision that makes gas accounting deterministic. But it has a consequence that no other mainstream language shares: in Solidity, absence has a value, and that value is a perfectly well-formed number.
Look at what that produces in practice.
mapping(address => uint256) public balances;
balances[someAddress] for an address that has never interacted returns 0. No revert. No log. If your code path is uint256 bal = balances[user]; if (bal < threshold) { liquidate(user); }, you have just written a function that liquidates everyone who does not exist.
That is the whole bug class in eight words: missing data read as a valid zero.
Now look at how it actually reached production.
In 2017, I was twenty-four, working out of an apartment in Copenhagen that I could not afford, and I spent seventy-two hours reading the Parity multisig library. Everyone remembers the July 2017 hack โ the delegatecall into an unprotected initWallet that let an attacker claim ownership of three wallets and drain roughly 150,000 ETH. What fewer people remember is that the November 2017 freeze โ about 513,000 ETH permanently locked โ came from the same root: a library contract that had never been initialized as a wallet itself. The owner slot was empty. Empty meant claimable. Someone called initWallet on the library, became its owner, and then called kill.
Absence of an owner, read as anyone may become the owner.
I published a breakdown of the reentrancy surface in the ERC-20 transfer patterns roughly forty-eight hours before the mainstream desks picked up the story. It made my name. But the Parity freeze is the case I keep coming back to, because it is the purest example of the pattern: nobody wrote a line of code that said if the owner is unset, allow anyone. They simply never wrote the line that said if the owner is unset, stop.
Nine years later, the same omission is sitting in the oracle integrations of eleven of the fourteen lending markets I audited this quarter.
Here is the code that everyone writes.
(, int256 answer, , , ) = priceFeed.latestRoundData();
require(answer > 0, "bad price");
uint256 price = uint256(answer);
And here is the code that almost nobody writes.
(uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
= priceFeed.latestRoundData();
require(answer > 0, "bad price");
require(updatedAt >= block.timestamp - HEARTBEAT, "stale price");
require(answeredInRound >= roundId, "incomplete round");
require(updatedAt != 0, "round not complete");
Four extra lines. The heartbeat constant is the one that gets skipped, because it is feed-specific and it changes. BTC/USD on Ethereum mainnet carries a one-hour heartbeat. ETH/USD carries one hour. On L2s the heartbeats stretch. If you fork Aave V3 onto a new chain and copy the HEARTBEAT constant along with the code, you have written a staleness check that is calibrated for a different feed on a different chain. It will pass your tests. It will pass a mock-based unit test with a fresh timestamp. It will fail in production at 3 a.m. on a Sunday.
Now the second flavor of absence, which is nastier because it looks like data.
Chainlink feeds carry minAnswer and maxAnswer bounds. These are circuit breakers. If the reported price crosses the bound, the aggregator stops reporting and the feed pins to the bound. This is not a failure. It is intended behavior. Chainlink published a note about it after 2022, warning integrators to check for it.
I spent two weeks of the 2022 bear market inside Terra's on-chain transaction logs, tracing the exact block where the UST peg decoupled from its ETH collateral, and building a forensic timeline. I found the arbitrage loop that most people still do not talk about โ a bot cycling between Curve pools and the mint/redeem path, each cycle extracting a fraction of a cent and each cycle pushing the peg a fraction of a cent further out. That timeline is still the thing I am most proud of publishing, and I cited every wallet address and every transaction hash so that readers could verify it themselves rather than trust me.
But while I was in those logs, I noticed something on the periphery. LUNA/USD feeds on several venues had pinned to their minAnswer floor. The price did not go to zero on-chain. It went to a floor, and it stopped. Protocols that had written require(answer > 0) were satisfied. The number was positive. The number was wrong by three orders of magnitude, and it was wrong in the direction that liquidates people.
That is the second flavor: a number that is present, positive, and structurally incapable of telling you it has stopped being true.
The third flavor is the one that will define 2026, and it is the reason I wrote this piece today.
In February I deployed a small test position โ about $2,000 โ into an AI-driven oracle network. The pitch is straightforward: instead of a committee of node operators signing a price, an LLM agent reads unstructured sources, reconciles them, and posts a signed attestation on-chain. The agent is the oracle. The reasoning is the product.
I ran it for six weeks. Latency was acceptable โ median 1.8 seconds from source publication to on-chain attestation, slower than a Chainlink round but faster than a human committee. Signature verification was clean. The contract was simple and I found no access-control issues.
Then I broke it on purpose.
I stood up a mock upstream source โ a fake exchange API endpoint that the agent was configured to read โ and I had it return an HTTP 200 with an empty JSON body. Not an error. Not a 500. A successful response with nothing in it.
The agent did not revert. It did not escalate to a human. It did not refuse to attest.
It wrote a number.
The number was within 0.4% of spot. Which is exactly what made it dangerous. It cleared the deviation threshold. It cleared the sanity bounds. It would have passed every check in the integration code I showed you above, including the updatedAt check, because the agent wrote a fresh timestamp.
An LLM's failure mode is not a revert. It is a plausible number.
I only caught it because I was cross-checking against a second source with a five-second poll, and the two diverged by 40 basis points in a market that was moving 8 basis points a minute. If I had been running this in production against a single source, I would have had no signal at all. The agent would have been, from the protocol's perspective, perfectly healthy. It would have been confidently, fluently, expensively wrong.
I want to be precise about the mechanism, because the phrase AI hallucination is a lazy phrase that hides the actual engineering problem.
A traditional oracle node has a narrow contract. It fetches a value. If the fetch fails, it returns failure. The failure is representable in the type system. A latestRoundData() call that has not been updated returns an updatedAt you can compare against.
An LLM agent has a generative contract. When you give it an incomplete input, the statistically most likely continuation of the prompt is a complete output. The model is not lying. It is doing precisely what it was trained to do: produce the most probable next token given the context. An empty context is still a context. The model fills it.
And here is the part that should worry every risk desk in this industry: generative completeness is a feature, not a bug, and it is completely invisible to the consuming contract.
answer > 0 passes. updatedAt is fresh. answeredInRound is valid. The attestation is signed by a key that the protocol trusts. Every invariant holds. The only thing that is wrong is reality.
Now layer this onto the liquidation path.
A liquidation bot reads the feed. It computes health factors across every open position. It submits a transaction for anything below the threshold. If the feed is stale-valid, the bot liquidates positions that are healthy at the real price, or refuses to liquidate positions that are insolvent. Both directions are losses. One direction is a loss to the borrower. The other is bad debt to the protocol.
In a bull market, bad debt gets socialized and then absorbed by the next wave of deposits. The protocol survives and everyone pretends it did not happen.
In a bear market, there is no next wave. Bad debt sits on the balance sheet, the utilization curve inverts, the withdrawal queue builds, and the LP base โ the people who were earning 4% to take this risk โ discover that they were the insurance policy. Over the past seven days I have watched one mid-cap lending market on Base lose 38% of its LP deposits in four days. The cause was not a hack. The cause was a liquidation that should not have happened, at a price that was forty minutes old.
Gas spike detected. Run. That is what the bots do โ when the sequencer comes back up after a stall, the pending queue flushes, gas goes vertical, and every liquidation bot in the mempool races for the same position. The winner is the one with the highest priority fee. The loser is the borrower, who gets closed at whatever price the first transaction happened to read.
Chainlink's answer to this is the Sequencer Uptime Feed plus a grace period. The recommendation is roughly an hour between sequencer restart and the resumption of liquidations. The reasoning is that after a restart there is a backlog of stale state and a burst of arbitrage, and the prices observed in that window do not reflect a live, liquid market. An hour of grace is an hour of exposure. Protocols trade it off. Most protocols, in my sample, set the grace period to zero because the parameter was not surfaced in the fork they copied.
Which brings me to a historical note that matters more than people think.
The reason Chainlink feeds became the reference in the first place is that AMMs made price a first-class on-chain object. Uniswap V2 moved the needle. Here is how: before V2, the price was something you asked an exchange about. After V2, the price was a cumulative sum stored in a contract that anyone could read. price0CumulativeLast and price1CumulativeLast were a public good. Every lending protocol that came after built its oracle stack on the assumption that price is a number you can read from a contract.
But an AMM's price is not a number. It is a function of reserves. And reserves can be zero.
getReserves() on an uninitialized or drained Uniswap V2 pair returns (0, 0). That is the third flavor of absence again: a structurally valid return value that encodes there is no market here. If your TWAP window includes a period where the pool was drained, your TWAP is a number that no market ever produced. V2's oracle design is actually careful about this โ the cumulative only advances when a swap occurs, and the block timestamp is packed in โ but the care is in the mechanism, not in the consumer. The consumer has to know to check.
The pattern repeats at every layer. AMM reserves. Oracle rounds. L2 sequencers. API payloads. LLM completions. In every case, the absence is representable, and in every case, the default representation is a number that looks fine.
Contrarian
Here is where I part company with most of the people writing about this.
The industry's answer to oracle risk is always the same: more feeds, more operators, more redundancy, more decentralization. Median-of-N. Staked attestations. Slashing.
That is the wrong axis. Redundancy does not fix a null-handling bug. If you aggregate three sources and two of them return absence-as-zero, your median is zero. If you aggregate three sources and two of them return a hallucinated number, your median is a hallucinated number โ and now it has a quorum, which means it has legitimacy. Redundancy converts a single point of failure into a correlated one. It makes the failure quieter and more expensive.
The fix is not more data. The fix is a control-flow change, and it is unglamorous: absence must halt execution. Not replaced by a fallback, not interpolated from the last value, not filled in by the model. Halted. revert. No liquidation, no mint, no borrow, no attestation. The protocol stops. Users can still withdraw what they deposited. That is the entire safety property.
The reason nobody ships this is that halting is commercially expensive. A protocol that halts during a sequencer outage misses liquidations it could have captured. A protocol that halts during a feed migration looks broken on the dashboard. A protocol that halts because its AI agent got an empty HTTP body looks, to a casual observer, like it is the thing that failed.
So the market rewards the opposite behavior. And that is the second half of the contrarian case: the incentive to hallucinate peaks in a bear market.
Every dashboard needs a number. Every indexer needs a row. Every fund report needs a TVL figure. Every AI agent needs an output, because an agent that returns nothing is an agent that gets replaced. In a bull market, there is enough real activity that nobody needs to invent any. In a bear market, the real activity is thin, the pressure to fill the gap is enormous, and the gap is exactly where the nulls live. The bug class will get worse in 2026, not better, and the driver is not technical. It is structural.
The audit industry will not catch it either. There is no exploit path to trace. Nothing reverts in the test suite. Nothing looks wrong against a mock that returns a healthy number. To find this, an auditor has to ask a question that has no code answer: what happens when the feed has nothing to say? Most audit scopes do not include that question.
And this is precisely why the institutional RWA narrative has never landed the way its proponents keep promising. A permissioned feed inside a bank has a null-handling procedure too. It is called a service-level agreement and a phone call to a named counterparty. When the data is missing, a human stops the process. There is no equivalent on a public chain โ no one to call, no contract that says wait. Which is fine, and which is also why the institutions building on public rails are building the parts that do not depend on trustless price discovery, and keeping the rest inside their own perimeter. The storytelling has run for three years. The data architecture has not moved.
Takeaway
If you hold a position in a lending market right now, go read the integration. Not the audit. The code. Find latestRoundData(). Count how many of the five return values are actually checked. Find the HEARTBEAT constant and ask whether it matches the feed and the chain you are on. Find the sequencer uptime check and look at the grace period.
If the grace period is zero, you are the liquidity.
And when the next batch of AI-oracle tokens starts trading โ and it will, inside the next two quarters โ remember what the mechanism actually is. ERC-20 rush vibes. Proceed with caution. The question is not whether the oracle is decentralized. The question is what it does when it has nothing to say.
Most of them, today, will say something anyway.