On a Tuesday in March I read a research report that contained no data. Nine analytical dimensions. Thirty-one tables. Every field returning the same placeholder string. Technical innovation: N/A. Token supply: N/A. Team: N/A. The risk matrix was twelve rows of N/A with a confidence rating of "unable to assess."
At the bottom: a disclaimer, and a request for the missing input set.
The honesty is what unsettled me. Nothing in that document lied. It just kept going. Somewhere upstream a parser choked on a table, a field mapper returned its default, and the pipeline did what every pipeline does when it loses input — it emitted. Nine empty sections. Zero analyses. One deliverable that looked, to a skimming reader, like a downgrade.
That's the most common failure mode in crypto infrastructure. Not a wrong number. A blank that renders as zero.
Walk the stack. An indexer pulls logs from an RPC endpoint. A decoder maps topics and calldata into typed structs. A normalizer joins those against price feeds and labels. A scorer reads the join and produces a rating.
Every hop carries a default for "I don't know." Python: None. JSON schema: null. SQL: NULL, which is not equal to itself. Solidity return: 0.
Those are four different semantics wearing one visual costume. In every front-end I have reviewed, the costume wins and blank rounds down to zero.
Now add the incentive. Indexers are paid for uptime, not coverage. A subgraph returning a partial response in 40ms looks better on a status page than one that errors after three retries. Errors page an engineer at 3am. Nulls get a green checkmark.
The industry measured what it could: latency, uptime, requests served. Nobody wrote an SLO for "fields we failed to populate." And the bear market thinned the operator set — pruned archive nodes, deprecated subgraphs, half the 2021 RPC fleet gone or deprioritized. Queries still succeed. Results get thinner.

Case one. Chainlink's latestRoundData() returns five fields. Nearly every integration I have audited checks the answer and ignores the timestamp. In my 2020 Compound v2 review I found an interest-rate overflow — an integer bug, and integer bugs are loud. Oracle bugs are quiet.
updatedAt is your staleness floor. Past the heartbeat, the feed is dead but still answering. It returns the last number it had. A liquidation engine reading that number computes a health factor from a price that stopped moving eleven hours ago, and when the market moved against it, the health factor is fiction in the safe direction.
answeredInRound was deprecated in the aggregator proxy. Deprecated, not removed. Code I read last quarter still gates on it. It returns the current roundId now, so the check passes vacuously. A guard that always passes is worse than no guard — it appears in an audit as coverage.
Case two. Aave-style getReserveData returns the full struct or a zeroed one. Iterate an address list pulled from a config file, and anything not yet listed comes back as zeros: zero liquidity, zero decimals, zero price. A dashboard computing "share of collateral in volatile assets" classifies the unlisted asset as stable, because zero volatility is what zero returns.
I profiled this across three deployments in 2024. Roughly 1 in 60 sampled collateral positions referenced an asset that returned a zeroed struct on at least one provider — not because governance hadn't listed it, but because that provider lagged the listing transaction. Two providers disagreed on the same block. Neither errored. The dashboard showed a book 40% larger than it was.
Case three. Liquidation bots are pure null-consumers. Read debt, read collateral, compute ratio, fire. Coerce null to zero in the numerator and the ratio goes to infinity and the bot skips. Coerce it in the denominator and the process throws and dies inside a loop nobody is watching.
I wrote one in 2021. The week I lost was not to math. It was a try block that caught a decode error, returned {}, and kept the loop alive. Six days of green logs, zero transactions. The chain didn't fail. The decoder did, and it did so politely.
Case four. Back to the report. Same defect, one layer up. The schema is strict about types and silent about presence. A string field accepts "N/A". A float field cannot, so an engineer under deadline makes it nullable — and nullable means the scorer sees None, and None propagates through nine layers of arithmetic until a formatter coerces it to display text. The type system tracked the absence faithfully for the entire pipeline and then discarded it at the last possible moment to produce a document.
In my 2025 AI-oracle work, non-deterministic model output broke consensus in 15% of transactions. That failure was loud, because the chain rejected it. The dangerous variant is the one the chain accepts. An agent that returns a plausible default instead of an error gets included, the state is wrong, and everyone downstream builds on it with full confidence.
The fix is boring, which is why it doesn't ship. Use a sentinel that cannot be confused with data. Not 0. Not null. A tagged union, and make the consumer match on it, so the unhandled branch is a compile error rather than a silent default. In SQL, split into two columns: value and provenance. No provenance, no join, no report. Refusing to generate is the correct behavior.
On-chain, return the tuple and force the caller to check updatedAt. If they don't, that's a fork. Forks are how people learn to read documentation.
The counterintuitive part is that our entire auditing apparatus hunts wrong answers. Formal verification proves a property holds. Fuzzing searches for inputs that break an invariant. The three-week penetration test I ran against an MPC custody implementation in 2024 produced 12 patches — every one of them for behavior that was incorrect.
Almost none of it probes for silence.
A null passes every invariant not explicitly written as "must be non-null." Fuzzing won't flag it; the code did exactly what it specified. The formal spec says "returns uint256," and zero is a uint256. Coverage tooling counts lines executed, not fields populated.
So: a system can be fully verified, 100% covered, and completely blind, with every dashboard green. The defect isn't in the code. It's in the gap between what the code guarantees and what the consumer assumed.
And the assumption never changes. That absence is a signal. That a blank means "we checked, there's nothing here." Nobody builds for the case where the blank means "we never got that far."
The next headline loss probably won't be a reentrancy bug. It will be a risk engine that read a zero, believed it, and sized a position. One question for every system you depend on this quarter: can it distinguish zero from unknown, in a way that actually stops the pipeline?
If the answer is a footnote at the bottom of the report, you already have it.