Over the past quarter, I logged a pattern across three lending markets that share one property: the liquidations were not triggered by price. They were triggered by the absence of price. An empty byte string returned from an external call. A null. Because the validation layer treated "no answer" as "zero," the arithmetic did the rest.
I have audited contracts since 2017. That year I spent six weeks manually tracing state transitions in Symbiont's asset tokenization protocol and found a reentrancy hole in the equity transfer function, a bug that could have drained user funds during volatility. That was a missing check. This is worse. This is a check that returns true when it should return false.
When the code bleeds, only the ledger survives. Here the ledger records real debt, real collateral, real liquidations. Only the price was absent. The absence was the exploit.
DeFi lending is a machine with two inputs and one output. Inputs: collateral value and debt value. Output: a liquidation decision. Everything between those points, the health factor, the liquidation bonus, the interest rate curve, is arithmetic stacked on a single number the protocol does not compute itself. It borrows it. From an oracle.
This is the architecture's central dependency, and it is rarely discussed in the right terms. Aave, Compound, Morpho, and every fork of them outsource the most important number in their system to a contract they did not write and do not control. The source varies: Chainlink push feeds, Pyth pull feeds, a Uniswap V3 TWAP, a bespoke governance-updated value. The trust assumption does not vary. The protocol assumes the feed will return a number. It almost never asks what happens when the feed returns nothing.
The two oracle designs differ in exactly the way that matters. Push oracles write prices on-chain on a schedule, so the protocol reads a stored value and a timestamp. Pull oracles require the caller to submit a signed price and a proof in the same transaction, so the protocol reads a value that arrived seconds ago. Push feeds fail by going stale. Pull feeds fail by not being pulled. Neither fails by politely returning zero, but both can hand the protocol an empty buffer if the integration is careless, and the protocol will decode that emptiness into whatever the ABI decoder decides, which is usually zero.
That question, what happens when the feed returns nothing, is not academic. It is the difference between a lending market that survives a data outage and one that liquidates its entire user base in a single block.
Consider the mechanics. A Solidity contract reads an external price feed with a low-level call. The call returns two things: a success flag and a byte array. The success flag tells you whether the call completed. It does not tell you whether the call made sense. A call to an address with no code returns success with an empty byte array. A call to a feed that has been paused returns success with an empty byte array. A call that lands on a fallback function returns success with an empty byte array.
Then the ABI decoder runs. It expects a uint256. It receives zero bytes. In many implementations, the decode of an empty buffer yields zero. No revert. No event. No alarm. The protocol now believes the asset is worth nothing.
From there, the math is obedient. If collateral is worth zero, the health factor is zero. If the health factor is zero, the position is liquidatable. The liquidation engine does exactly what it was written to do. It seizes collateral that was never worthless and sells it into a book that never saw the null. The liquidator collects the bonus. The borrower loses the position. The protocol records a successful, well-formed liquidation against a price that never existed.

The exploit is not a bug in the price. It is a bug in the interpretation of silence.
There is a second layer, and it is where most audits stop looking. Stale is not the same as empty. A feed that is late returns an old number. A feed that is silent returns no number. These require different defenses, and most protocols only build one.
A stale feed is a timing problem. Chainlink's push feeds update on a heartbeat and a deviation threshold. If the heartbeat is one hour and the deviation is half a percent, the feed can sit unchanged for an hour and still be fresh by design. The standard mitigation is a staleness check: read updatedAt and revert if it is older than a threshold. Simple. Common. Documented. The deprecated answeredInRound field caused its own confusion for years, because a value could be answered in a round that never completed, and integrators copied the check without understanding it.
A silent feed is a structural problem. There is no timestamp to check because there is no answer. The staleness guard reads updatedAt, gets zero, and, if the guard itself is sloppy, passes, because zero is not older than a threshold in every comparison an engineer might write. Or the guard reverts, the circuit breaker trips, and now the market is frozen with open positions and no way to value them. Neither outcome is safe. One is a liquidation cascade; the other is a liquidity trap.
The protocols that handle this well do one specific thing: they treat an empty return as a first-class failure, not as a zero. They check the success flag and the buffer length together. They revert on ambiguity and fall back to a secondary source with an explicit, logged switch. They do not catch and continue. That is unglamorous engineering. It is also the entire difference between a survivable incident and a terminal one.
Which brings me to try/catch, the most overrated safety feature in Solidity. Catching an exception and then proceeding with a default value is the same null-as-zero bug wearing a seatbelt. The exception was the signal. Swallowing it and continuing with zero, or 1e18, or the last cached price does not make the protocol resilient. It makes the failure quiet. Resilience is not the absence of a revert. It is the correct response to one.
Now widen the lens, because this is not only an oracle problem. The same class of failure lives inside the interest rate models that every lending market runs on.
Here I will be direct: the interest rate models in Aave and Compound are arbitrary. They are piecewise-linear curves drawn by governance, tuned by committee, and defended with the language of market efficiency. They have almost nothing to do with real supply and demand. They are a policy dressed as a mechanism. The kink, the slope, the base rate, all chosen, none discovered.

That matters for empty data because the rate model consumes utilization, and utilization is a ratio: borrowed divided by supplied. Feed the model a corrupted denominator and it will not complain. It will compute. If total supply reads as zero, because a share token's balance call returned empty, then utilization is undefined, and depending on how the division is ordered, the result is either a revert or a division by zero that the EVM turns into a panic. A panic in a view function that a liquidator calls mid-cascade is not a safety feature. It is a stuck market at the exact moment liquidity is most needed.
The deeper problem is that the model was never designed to be robust to bad inputs, because the people who drew the curve assumed the inputs would always be good. A mechanism that is only correct on valid data is not a mechanism. It is a happy path with a governance token attached.
Yield is the shadow cast by risk taken. When the shadow looks wrong, check the light source, not the shadow. In a lending market, the light source is the oracle and the input validation. Everything downstream is reflection.
Then there is composability, which turns a single null into a systemic event. A lending market does not sit alone. It sits inside vaults, aggregators, recursive strategies, and structured products that read its state as an input to their own. One bad price does not stay local. It propagates.
Trace the path. A yield aggregator reads the lending market's exchange rate to price its own shares. The exchange rate is derived, in part, from the market's total assets, which depend on the oracle. The oracle returns empty. The exchange rate collapses. The aggregator's own oracle, often a share-price feed built directly from that exchange rate, now reports a distorted value. A second protocol uses the aggregator's token as collateral. Now the null has a grandchild, and the grandchild has leverage.
This is the part of money legos nobody puts on the slide. Composability is a multiplier on correctness. It is also a multiplier on failure. When the base layer is right, every layer above amplifies the rightness. When the base layer is wrong, every layer above amplifies the wrongness, faster, with more leverage, and with less human oversight at each hop.
I do not trust whispers; I trust verified hashes. The verified hash here says the call succeeded and the buffer was empty. The verified hash does not say the price was zero. The gap between those two statements is where the money is lost, and it is a gap that no dashboard will ever show you, because the dashboard was built to render numbers, and zero is a number.
So why does this keep happening? Because audits optimize for the happy path. A fuzzer generates valid inputs. A unit test asserts that a known price produces a known health factor. Almost nobody writes the test that asks: what does this contract do when the oracle returns nothing, when the feed is paused, when the address is an EOA, when the buffer is empty, when the timestamp is zero, when the returned value is stale by one block?
That test is boring. It does not demo well. It does not produce the critical-severity headline that wins the engagement. It produces a medium-severity note that a founder reads once and defers. Then, eighteen months later, during a maintenance window or a chain upgrade or a sequencer outage, the feed goes quiet, and the deferred note becomes the incident.
Migrations are just purgatory for lazy capital. The same is true of validation logic ported from V2 to V3 to V4 without anyone re-reading the null path. The code moves. The assumption does not get re-examined. The assumption was wrong the first time.
Now the contrarian part, because the popular framing is backwards.
Retail watches the UI. The UI shows a number. That number is cached, or defaulted, or interpolated, or last-known-good. The dashboard renders a green candle of confidence over a feed that may be dead. The interface is a translation layer that hides exactly the state that matters most.
Smart money watches the oracle contract and the validation branch. It reads latestRoundData, checks answeredInRound and updatedAt, and asks whether the protocol's guard would actually catch a silent feed. It does not ask, is the price right. It asks, what happens if there is no price. That is a different question, and it is the one that determines survival.
The blind spot is universal: everyone assumes the oracle always answers. The entire risk surface is priced for wrong answers, not missing ones. Liquidation bots are tuned for price moves. Insurance is underwritten for price moves. Governance debates are about price moves. The null case is invisible because it produces no signal until the exact moment it produces liquidations.
And to be clear about a fashionable non-fix: intent-based architectures do not solve this. They relocate it. When you move execution from an on-chain AMM to an off-chain solver network, you have not removed the failure mode. You have moved it somewhere you cannot read. The solver's internal price source can return empty, and the on-chain contract will simply see a signed order that claims the trade is valid. The MEV did not disappear either. It migrated off-chain, into the solver's mempool, where visibility is worse and accountability is thinner. A null that happens off-chain is a null you will never audit.
So what do you actually watch, sitting in a sideways market with capital deployed and no clear direction?
You watch the guard, not the price. Pull the protocol's oracle adapter and read the validation branch. Does it check the success flag and the buffer length, or only one of them? Does it revert on staleness, against what threshold, and is that threshold longer than the feed's own heartbeat? Does it have a fallback source, and does the fallback emit an event when it activates? If the answer to any of these is "I think so," you do not have a position. You have a hope with an APY.
You also watch the maintenance calendar. The quiet feed is usually a paused feed, and feeds get paused during upgrades, migrations, and chain-level incidents. The window of maximum danger is the window of maximum change. Chaos is just data waiting for a ledger. The protocol that logs its fallbacks is the protocol that will tell you, after the fact, exactly how close it came. The protocol that stays silent about its fallbacks is the one you should not be lending into.

The lesson from a decade of this is narrow and durable: the most dangerous input in DeFi is not a wrong number. It is no number. A wrong number produces a bad trade. No number produces a silent cascade, because the system was built to compute on whatever it receives, and zero is a perfectly computable value.
When the code bleeds, only the ledger survives, and the ledger will show that you were liquidated by arithmetic that was never wrong. Only the input was missing.
Watch the null. It is the quietest exploit in the building, and it does not need a headline to empty a market.