Last week a research pipeline returned a nine-field report. Every field read not provided. No exception. No stack trace. Exit code zero.
I read it twice before I understood what I was looking at. Not a failure of extraction. A failure of refusal. The system had been asked for values it did not possess, and rather than stopping, it emitted a well-formed empty — nine plausible fields, correctly typed, structurally valid, and entirely hollow.
That is the same decision that drained roughly $190 million from Nomad on August 1, 2022. Same shape. Different layer.
The most dangerous output in crypto infrastructure is not an error. It is a plausible zero. Errors propagate. Zeros settle in. They get written to a database, charted, aggregated into a risk dashboard, and forwarded to whoever asks next — now laundered into something that looks like an observation.
Math doesn't lie. But math applied to a zero that should have been a revert will lie to you all the way to liquidation.
The chain of custody for numbers
Every crypto system is a chain of custody for numbers. A sequencer produces a state root. An indexer reads logs. An oracle posts a price. A dashboard sums collateral. At each hop, the consumer must decide what to do when the producer hands back nothing.
There are two ways to encode "nothing." One is to revert — to refuse, loudly, in a form the caller cannot ignore. The other is to return a default: zero for a uint, false for a bool, an empty array, address(0), a struct with every field unset.
The second is cheaper. It is also the default in almost every layer crypto is built on.
Solidity has no null, no Option, no Result. An uninitialized storage slot reads as zero. A mapping lookup for a missing key returns zero rather than throwing. address(0) is a legal value that compiles cleanly and passes type checks. Emptiness and legitimacy share a type.
Off-chain the situation is worse, because the coercion is invisible. JSON parsers map missing fields to null. GraphQL returns data: null next to an errors array that most clients never inspect. Subgraph queries time out and the client renders an empty list as "no activity" rather than "unknown." A decomposer handed zero input returns nine fields of N/A instead of DECOMPOSITION_FAILED.
Each of those is a local convenience. Aggregated across a stack, they become an epistemic hazard: the system cannot distinguish "we measured zero" from "we did not measure." In a bull market nobody checks. In a bear market, that distinction is the difference between a solvent position and a liquidated one.
I ran into this directly while building the 2024 spot-versus-futures ETF arbitrage model. The alpha was never in the premium calculation; it was in the ingestion layer. A NAV feed that returns a stale print is usable. A NAV feed that returns nothing, parsed into a zero premium, generates a signal that looks like the best trade of the quarter and is actually a missing row. I ended up writing more validation code than strategy code. That ratio has held ever since.
Nomad and the zero root
The Nomad bridge upgrade is the cleanest case study available. In Replica.initialize(), the contract marked a message root as confirmed: confirmAt[0x0] = 1. The prior version had derived that initial root from configuration. After the upgrade, the zero hash was pre-approved as valid.
process() then accepted any message whose first 32 bytes — the root field, attacker-controlled — matched a confirmed root. Zero matched. Therefore any message matched. Within hours, people who had never written a line of Solidity were replaying a copied transaction with a swapped address and draining the bridge.

No cryptographic break. No key compromise. The proof system did exactly what it was specified to do. The specification simply included zero in the set of acceptable proofs. Absence of evidence was encoded as evidence, and the encoding was a single typo in an initialization line that passed review because it looked like boilerplate.
The empty return that says yes
Consider the low-level call. (bool ok, bytes memory ret) = target.call(data); If target is an EOA, a self-destructed contract, or an address that never had code, ok is true and ret is empty. The EVM is not lying. It successfully did nothing. Code written as require(ok) believes it verified something.
Now invert it. USDT's transfer returns no value at all. An interface declaring returns (bool) that calls it will revert on the empty returndata — which is why SafeERC20 exists and why every serious integration uses it. Two failures, opposite directions, one root cause: an interface that assumed a shape the producer never promised.
Euler's March 2023 loss of roughly $197 million is the adjacent variant. donateToReserves was missing the checkLiquidity modifier that its sibling functions carried. The absence of a check was read as permission. Attackers do not need to break your logic. They need to find the one path where the logic was never wired up.
Chainlink's uninitialized round
latestRoundData() returns five values. When a feed is uninitialized, deprecated, or has never posted, it can return roundId == 0 with answer == 0. It does not revert. The integrator is expected to check roundId != 0, updatedAt != 0, and answeredInRound >= roundId. Most do not. Those who do usually stop at staleness and never touch the zero case.
A price of zero is not a rounding artifact. In a lending market, a zero collateral price triggers mass liquidation. A zero borrow price implies infinite borrowing capacity. The same bytes, two catastrophes, decided entirely by which side of the ledger reads first.
What the Aave v1 latency model actually showed
In 2020 I built a quantitative model of Aave v1's price feed to see what the protocol did during the window between an oracle update and the market's response. The finding was structural rather than dramatic. The risk engine consumed the last valid price and had no representation of "price unknown." There was no state for it. A stale price and a fresh price were the same type, the same variable, the same input into the health factor.
That is the real vulnerability class — not a missing check, but a missing state. If your contract cannot express uncertainty, every value it holds is a claim of certainty. In a bear market, when feeds update less reliably and order books thin out, the number of moments in which the system is confidently wrong goes up, not down.
The dashboard that reports health by default
Then there is the reporting layer, where the plausible zero becomes a governance problem. I have watched a protocol's TVL chart draw a clean line straight through a nine-hour indexer outage. The number did not drop. It flatlined at the last value and resumed. The chart looked healthy. The data was absent.
Every N/A in a risk report is a liability transfer. The protocol did not answer, so the reader supplies an assumption — almost always the comfortable one. That is how counterparty risk accumulates without anyone making a bad decision.
The autonomy multiplier
This is the part that should worry the market more than any case above. In auditing three AI-agent protocols this year, I found that agent execution loops consume oracle and indexer data directly, with no human reading a dashboard in between. An agent has no prior about plausibility. Hand it a zero and it will not pause to wonder whether the feed is down. It will size a position, post collateral, or unwind a hedge, because zero is a number and numbers are instructions.
Autonomy converts a data-quality bug into a market event at machine speed. A human notices that BTC is not $0. An agent executes.
Checks do not compose
The reflexive fix is to add a require. require(roundId != 0). require(ret.length > 0). Wrap it in a linter rule and move on. That is the wrong layer.
A check is a form of documentation, and documentation does not compose. A check added in module A protects module A. The bug reappears in module B, written by an engineer who joined after the post-mortem was circulated. I have now seen the same missing staleness check ship from four separate teams, each of which had read the same audit report and agreed with it.
The correct fix is to make the empty case unrepresentable, not to remember it. Make the function revert when the data does not exist. Make the type carry the distinction — Option, a status enum, a discriminated union, anything that forces the caller to handle absence before the code compiles. Make the indexer return an error status instead of an empty array. Make the decomposer return DECOMPOSITION_FAILED instead of nine hollow fields.
This is the blind spot in audit culture. Almost every review is aimed at logic — arithmetic, access control, reentrancy, rounding direction. Very few are aimed at the boundary where the system decides whether it knows something at all. That boundary is where the money leaves.

There is a bear-market corollary nobody wants to hear. The protocols currently showing the cleanest risk dashboards are not necessarily the safest. They may simply be the ones whose reporting layer is most polite about its own ignorance.
What to ask instead
The next time a counterparty hands you a risk report, do not ask what the numbers are. Ask what the system returns when it has nothing. Ask whether that return is an error or a zero. Ask whether the dashboard is capable of rendering "unknown" — and if it cannot, assume it is rendering something else.
Code is law, until it isn't — and it stops being law the moment it starts answering questions it was never asked. The protocols that survive this cycle will not be the ones with the most audits. They will be the ones whose empty set is loud.