Last quarter a research pipeline I was asked to review returned forty-seven fields. Forty-seven N/A.
The scoring module downstream read every one of them as zero risk and assigned the asset a composite grade of A-minus. Nobody had tampered with the output. Nobody had lied. A scraper had failed to authenticate against an API that rotated its keys six weeks earlier, and every layer above it was built to tolerate missing values rather than to refuse them. The report ran ninety pages. Three of them contained information.
I have been auditing this class of problem since 2018, when I spent three months inside the 0x v2 relayer contracts and found seven edge-case vulnerabilities that all traced back to the same root: an assumption that an empty response meant an empty state. This article is about that assumption. It is not about the pipeline that broke. It is about the shape of the hole it left, and why that shape is almost always read as a green light.
Trust is a vulnerability, not a virtue. The corollary is worse: a missing observation is not an observation of absence. It is a fault in the instrument. And the industry has spent a decade building instruments that lie by omission, then congratulating itself on its caution.
The architecture of the hole
Every research pipeline in crypto has five stages. Ingestion pulls from RPC endpoints, indexers, APIs, scrapers. Parsing turns bytes into structured records. Entity resolution binds a deployer address to a legal entity to a GitHub handle. Scoring reduces the records to a number. Narrative turns the number into prose.
Nulls are created at stage one โ network failures, rate limits, rotated keys, chain reorganizations, endpoints that return 200 OK with an empty body. Parsed at stage two into a field that is present and empty, which is not the same as a field that is absent. Resolved at stage three into an entity with no attributes, which the resolver treats as a new entity rather than an unresolved one. Coerced at stage four. Smoothed at stage five.
The coercion is where the money goes. A weighted composite score is a sum: score = ฮฃ wแตขxแตข. When xแตข is missing, the pipeline substitutes something. It substitutes 0 if it is optimistic about absence. It substitutes the cross-sectional mean if it wants to be neutral. It substitutes the last known value if it is lazy. Each substitution is an assertion, not a default.
Substituting 0 asserts this unknown quantity is zero. Substituting the mean asserts this unknown quantity is typical. Neither is derived from evidence. Both are derived from the shape of the data structure. This is exactly the behavior of Number(null) in JavaScript, which evaluates to 0 โ a language-level decision to make absence numerically indistinguishable from nothingness. Every analytics stack in this industry is a JavaScript runtime with a compliance department.
Math doesn't negotiate with your schema. A blank field is not a low value. It is a different type.
The taxonomy of nothing
Before the analysis, the vocabulary. The entire failure mode lives in the difference between things engineers treat as identical.

Consider the EVM. A CALL to an address with no code returns success = true and zero bytes of return data. Not an error. Not a revert. Success. This single design decision produced a generation of exploits. The ERC-20 standard specified that transfer returns a bool, and a large fraction of deployed tokens โ USDT being the most famous โ return nothing at all. Contracts that wrote require(token.transfer(to, amount)) were calling a function that returned zero bytes, then decoding those zero bytes as... nothing, and depending on compiler version and declaration, either reverting on a failed decode or reading an uninitialized boolean off the stack.
The fix is SafeERC20. It exists because the compiler, the ABI decoder, and the developer all disagreed about what an empty response means. Three parties, three semantics, one shared assumption that absence could be read as consent.
The 2020 cycle added the oracle version. Chainlink's latestRoundData() returns five values: roundId, answer, startedAt, updatedAt, answeredInRound. I have read a lot of integrations. The overwhelming majority consume answer and discard the rest. That is the bug. answer is a number sitting in storage. It has no timestamp, no status, no memory. If the feed has not updated in eleven hours because the aggregator's OCR2 transmission threshold was not met, answer still returns the last transmitted price, and it returns it with full confidence.
The defense is three lines:
(, int256 answer, , uint256 updatedAt, uint256 answeredInRound) = feed.latestRoundData();
require(updatedAt >= block.timestamp - HEARTBEAT, "stale");
require(answeredInRound >= roundId, "carry-forward");
require(answer > 0, "unset");
Almost nobody writes the second one. answeredInRound handles the case where a round was opened and never answered โ the aggregator advanced roundId without producing a value, and answer is carried forward from an older round. On paper the feed is fresh. In storage it is a fossil.
Then there is the sentinel problem, which is worse. Chainlink feeds ship with minAnswer and maxAnswer circuit breakers. During the May 2022 collapse, the LUNA/USD feed on BNB Chain hit its minAnswer near $0.10 while the market price was under a cent. Venus read the feed and did exactly what the code told it to do: it valued collateral at $0.10 and liquidated borrowers who were, at true market price, still solvent. Somewhere north of $200 million in positions unwound against a number that was not a price. It was a boundary condition, mislabeled as data.
This is the taxonomy. null. undefined. 0. address(0). Empty return data. Stale round. Clamped round. A sentinel standing in for "we don't know." Five of these six are representable as a valid integer in every scoring system I have reviewed.
In 2018 the same lesson arrived through 0x. Three months inside the v2 relayer logic, tracing the atomic swap path line by line. Seven findings, all edge cases, all in the branch the test suite never covered. The order hash verification was fine. The signature scheme was fine. What was not fine was what happened when an order field was present but empty โ a zero-maker-asset, a zero-fee-recipient, a takerAssetAmount that decoded to zero. Each of those zeroes had a valid meaning somewhere in the code and a different valid meaning somewhere else, and the contracts could not tell them apart. I sent the findings straight to the repository, because that is where the code lives and that is where the argument has to be had.
What the ceremony taught me
In 2020 I spent a month inside the Groth16 trusted setup transcripts for Zcash's Sapling circuit. Not the ceremony's marketing โ the transcripts. The actual contribution hashes, the attested participants, the sequence of the beacon.
The relevant property is this: soundness of the proof system assumes at least one participant honestly destroyed their toxic waste. If all participants collude, the trapdoor exists and the proofs are forgeable. So the security parameter is a function of the set of verified contributions. Remove one and the assumption degrades. Lose the record of one and it degrades to the same place, because you can no longer prove that participant was honest.
Here is the point that took me a long time to articulate. In a ceremony, a missing contribution is not a missing data point. It is a hole in a security assumption. The protocol does not become slightly less verified. It becomes verified under a strictly weaker claim, and the strength of that claim is not computable from the transcript alone.
The identical structure appears in audit reports. A report with an N/A in the access-control row is not a report that scored zero on access control. It is a report whose access-control assumption is now unstated. The reader sees a blank and infers "no issues found." The actual meaning is "no issues were searched for."
Privacy is a protocol, not a policy. Verification is the same. It is a property of a specific, complete set of observations. Remove an observation and you have not removed a policy. You have removed a protocol and replaced it with a feeling.
The expected observation
There is one more idea, and it is the one that separates forensics from filing.
Absence of evidence is not evidence of absence. Everyone repeats this. Almost nobody operationalizes it. The operational version requires that you state, in advance, what you expected to see. An observation is only informative relative to a prediction.
If your hypothesis is "the team is anonymous and the treasury is a multisig," and you look and the deployer address has no history, that is consistent with the hypothesis โ zero information gained. If your hypothesis is "this is a protocol with a live mainnet deployment and weekly transactions," and you pull the contract's event logs and find nothing since deployment, you have not found an absence of evidence. You have found a refutation. The expected observation was specified, searched for, and not there. A blank field is evidence only when it was predicted to be full.
Practically, this means every N/A needs a paired expected value. What did you expect this field to contain, and under what hypothesis? A field with no expectation attached is not a finding in either direction. It is decoration. And a framework composed entirely of unmatched fields is not a conservative assessment. It is a document whose only real content is the fact that nobody looked.
The 500 contracts and the rounding error
Between 2021 and 2022 I read mint functions. Not the marketing pages โ the functions. Somewhere north of five hundred contracts. Most were copies of copies. The interesting ones were the third-generation derivatives, written by people who had read the first two generations and decided they could do better.
The class of bug I kept finding was arithmetic truncation in the price calculation. A typical mint:
uint256 cost = (quantity * pricePerToken) / 1e18;
require(msg.value >= cost);
Solidity integer division truncates toward zero. Choose quantity such that quantity * pricePerToken < 1e18 and cost is 0. The require passes. You have minted for free. Whether it is exploitable depends on the constraint upstream โ and upstream, the constraint was usually quantity > 0.
One contract in the CryptoPunks derivative ecosystem had a worse variant. Its supply-cap check used a strict inequality against a constant one higher than the array bound, and the mint loop was bounded by the user-supplied quantity rather than the remaining supply. Two off-by-ones, composed. The result was a state where totalSupply could briefly exceed the cap, and any downstream contract reading totalSupply as the index for a subsequent mint would write into uninitialized storage.
I wrote it up and sent it to the team. I received an acknowledgment. The fix did not ship for four months, and by then the contract had been forked eleven times. This is the normal outcome. The incentive gradient points at the deploy button, not the review queue.
Terra, and the reserve that was a flow
The Terra collapse gets described as a failure of an algorithmic stablecoin. That framing is imprecise, and it lets the interesting part escape.
The mechanism was this: UST was redeemable for LUNA at the oracle price of LUNA. System solvency depended on the marginal price of LUNA, which depended on the marginal buyer of LUNA, which depended on the yield paid to hold UST, which was funded by the growth of the LUNA supply. The growth rate had to exceed the redemption rate. That is not a stability property. It is a growth condition. It holds while the second derivative is positive and stops holding, discontinuously, when it isn't.
The published reserves were not hidden. The on-chain balances were visible the entire time. What was missing was a stock. The reserve was a flow โ an inflow measured per unit time, reported without its time derivative, and read by the market as a level.
So the N/A in that case was the same N/A. The balance sheet was published in a format where the load-bearing quantity โ the rate at which new capital had to arrive โ was absent, and the absent quantity was implicitly scored as zero risk. Twenty thousand words of post-mortem later, the game-theoretic summary is one sentence: a system whose solvency depends on an unobserved inflow will be scored as solvent until the exact block it isn't.
Fail closed, or don't bother
There is a correct pattern, and it is not novel. It has been in type systems for decades.
Rust makes absence representable and unignorable. Option and Result are not nullable pointers; they are sum types, and the compiler will not let you reach the payload without a match, a ?, or an explicit unwrap that leaves a trace. Ignoring a missing value requires writing code that says you are ignoring it.
Solidity 0.8 added checked arithmetic. It did nothing about missing inputs. You can require on staleness, but the language will not make you.
Python and JavaScript chose the opposite. None propagates through arithmetic as a TypeError at best and as a silently coerced numeric at worst. undefined becomes NaN, the only value in IEEE 754 that is not equal to itself โ a value engineered to destroy comparisons. It is a beautiful design. It is also why your dashboard rendered a green tile.
Correct handling is one of three things, and they are not interchangeable.
Refuse. Do not produce a report. Return an error up the stack. This is revert. It is the only option that preserves the information that something was missing.
Propagate. Carry the absence explicitly to the consumer with its own type. Report unknown, not 0. Make it impossible for the end user to see a number where there is none.
Substitute โ and label it. Use an estimate, and attach provenance and uncertainty. Never let a substituted value enter the same accumulator as a measured value without a weight reflecting how much was invented.
What the industry does, almost universally, is option four: substitute silently, then report the output of the accumulator as though all of its inputs were measured.
The contrarian read: blank is worse than wrong
Here is the part that gets me uninvited from rooms.
The consensus position on blank frameworks is that they are the honest output. The analyst who writes N/A - information insufficient is being rigorous. The analyst who invents a plausible number is being a fraud. The first is praised; the second is punished.
That ranking is backwards, and the reasons are structural.
A fabricated report has an attack surface. The number can be checked against the contract. The claim can be falsified by anyone with an RPC endpoint. A fabrication is a lie that can be caught, and liars know it, so they keep their inventions in the range where detection is expensive.
A blank report has no attack surface. There is nothing to check. And crucially, the blank report converts a pipeline failure into a deliverable. The scraper broke. The keys rotated. The indexer was down for six hours. None of that appears anywhere in the output, because the output is a framework, and frameworks look like work. The report is filed. The checkbox is ticked. The process risk โ the single most common cause of institutional blindness in this industry โ is laundered into a document that reads as caution.
Then there is the incentive layer. Output volume is the metric. An analyst is evaluated on pages shipped, dashboards built, threads posted. The specialist who writes "cannot assess; upstream ingestion failed; here is the diff of the API response" ships one paragraph and looks unproductive. The generalist who fills eighty fields with estimates ships a deck and gets promoted. The equilibrium is not subtle. The market pays for coverage, not for nulls. So it gets coverage, and the nulls are still there, wearing the shape of coverage.
And the third option โ actually repairing the instrument โ has no deliverable at all. Nobody ships a memo titled "we fixed the scraper." The fix is invisible. The break was invisible. The blank report is the only artifact that survives, and it survives looking responsible.
I have watched this at three funds and one protocol foundation. Identical pattern every time: a data outage, a template output, a sign-off, and a discovery six months later that the entire period's assessments were derived from a field that was empty on every single row.
Takeaway
The next large loss in this cycle will not be a reentrancy bug. Reentrancy is solved, taught, and instrumented. It will not be a private key leak, because custody has been engineered into a solved problem at the institutional tier.
It will be an N/A that somebody promoted to a number.
Three signals to watch over the next twelve months. The ratio of framework to finding in published audits โ a forty-page document with three findings is a document whose remaining fields were never populated, and the tokens launched on top of it are priced as though they were. The oracle integrations that read answer and discard updatedAt and answeredInRound โ that population is a liquidation cascade waiting for a quiet Saturday. And the treasuries whose published balances exceed their verifiable on-chain holdings, where the difference is the exact size of the compliance shield.
Math doesn't care which field you left blank. The sum runs anyway. Somewhere right now a green number is being computed from a column of holes, and it is being read by someone who has already decided what it says.