N/A Is Not a Status Code: The Silent Failure of Empty-Input Systems

CryptoWolf
Security

The report came back clean. Nine dimensions. Every field present. Every header rendered. Technical analysis: N/A. Token economics: N/A. Market structure: N/A. Governance: N/A. Risk matrix: N/A. A complete analytical framework, executed end to end, returning a document that said nothing at all.

That is not the failure. That is the symptom. The failure is that the pipeline ran at all.

I have spent eleven years reading smart contract failures, and the ones that kill people are never the ones that throw. They are the ones that return. A function that reverts is a function that told you the truth. A function that returns zero is a function that lied and made you ship it to production. The document on my desk is a zero. It is a perfectly formatted, nine-section, confidence-rated, fully annotated zero. And the most dangerous line in the entire thing is the one where the analyst writes, correctly, "N/A - insufficient information" — because everything around that line was designed to make you believe the analysis was completed.

Context: What the Document Actually Is

Let me describe the artifact before I dissect it, because the shape of it matters more than the content. This is a second-stage analysis report. The architecture is two-stage by design: a first stage that deconstructs a source article into structured facts — information points, core claims, project entities, source quality — and a second stage that consumes those facts and produces nine dimensions of judgment. Technical. Token economic. Market. Ecosystem. Regulatory. Team. Risk. Narrative. Supply chain.

This is a sensible pipeline. It is the same architecture I have built into internal tooling for reading whitepapers, and it is the same architecture every serious research desk in this industry converges on, because unstructured text is where human bias lives and structured facts are where you can actually audit your own reasoning. The first stage is the extraction layer. The second stage is the inference layer. Clean separation. Good design.

What happened here is that the extraction layer returned empty. The information-point list — the single field that every downstream conclusion is supposed to cite — came back as an empty array. Not corrupted. Not truncated. Empty. And when the inference layer received an empty array, it did not crash. It did not raise. It did not halt the job and page a human. It walked the nine-dimension framework, populated every cell with the string "N/A - insufficient information," computed a five-star information-value rating of one star per dimension, and emitted a report.

Here is the part that earns this document a place in my files. The report is honest. Somewhere in the middle, the analyst writes the sentence that should be carved above every data pipeline in this industry: any technical, token-economic, or market analysis built on this input would degrade into pure speculation, which violates the core principle of not trusting surface narratives and not making unfounded inferences. And then, having written that, the analyst refuses to fill the blanks. Refuses to guess. Refuses to invent a project, a team, a token model, a competitor set.

That refusal is correct. I want to be very clear about that, because I am about to be harsh about everything around it. The refusal is the one part of this artifact that worked. The problem is that the refusal happened in the wrong layer.

Core: The Null Propagation Problem

Start with first principles. A data pipeline has one job: to transform inputs into outputs such that the outputs are falsifiable against the inputs. Every conclusion must trace to a source. When I write that a contract has an integer overflow in its vesting logic, I can point to the function, the line, the arithmetic. When I write that a gas optimization saved 22%, I can point to the diff and the measurement. That traceability is not a stylistic preference. It is the entire difference between analysis and astrology.

Now feed that pipeline an empty input. What should happen?

The correct behavior is a hard stop. If the extraction layer produces zero information points, the inference layer should never be invoked. The job should terminate at the boundary with a non-zero exit code, and the exit code should say what every operations engineer knows it should say: the upstream dependency returned nothing, and nothing is not a valid basis for anything. This is not a sophisticated requirement. It is the same requirement that governs every latestRoundData() call in every oracle integration I have ever audited.

Let me be concrete, because this is where the abstract becomes the expensive.

The Oracle That Returns Nothing

When you call a Chainlink price feed, you do not get a price. You get a round. The round contains an answer, a timestamp, and a round ID, and the round ID is the important part because it is the part that tells you whether the answer is current. The canonical integration check looks like this:

(, int256 answer, , uint256 updatedAt, uint256 answeredInRound) = feed.latestRoundData();
require(answer > 0, "bad answer");
require(updatedAt != 0, "round not complete");
require(answeredInRound >= roundId, "stale round");

Three requires. The first checks that the number is a number. The second checks that the round finished. The third checks that the round you are reading is not older than the round you think you are reading. I have audited lending markets, perp DEXs, and structured-product vaults that implement exactly one of these three checks. Usually the first. Almost never the third.

Why does the third matter? Because a stale round is an empty round wearing a number. The feed returns a value. The value is real — it was real once, at a timestamp that is now hours or days in the past. The contract reads it, believes it, and prices a liquidation against a price that has not existed since the last time the network was healthy. That is the empty input. It is not zero. It is worse than zero, because zero fails the require and stale passes it.

I watched this pattern take down a perp venue in 2022. The feed was fine. The integration was fine, in the sense that it compiled and it passed every unit test the team wrote, because every unit test mocked latestRoundData() to return a fresh, sane round. The test suite never once returned a stale round. It never once returned answeredInRound < roundId. The mock was too helpful. The mock filled the blank.

Code that doesn't fail in testing isn't ready for mainnet reality. The mock is the enemy of the audit.

The TWAP With No Observations

Same disease, different organ. Uniswap V3's oracle is not a feed. It is a ring buffer of cumulative tick observations, and you get a time-weighted average by calling observe() with a window. If the window you request extends further back than the oldest observation in the buffer, the call reverts with the string OLD. That revert is a gift. It is the protocol telling you, explicitly, that it cannot answer your question with the data it has.

The gift gets refused constantly. I have read integrations that wrap the observe() call in a try/catch and return a default. I have read integrations that catch the revert and fall back to the spot price, which is exactly the number the TWAP existed to smooth out. I have read one — one, in an audit last year — that caught the revert and returned 0, then divided by that zero three functions later, which is how you get a division-by-zero panic that reverts the entire transaction and looks, in the logs, like a gas estimation problem rather than an oracle problem. The team spent two days chasing gas.

The gas isn't the problem. It's the friction of poor architecture. The zero was the problem. The zero propagated because nobody put a boundary at the edge of the system and said: if this returns nothing, stop.

Three-Valued Logic and the Lie of the Default

Step back to the database layer, because the database layer is where this failure mode was formally studied decades before anyone wrote a smart contract. SQL has three-valued logic. A comparison against NULL does not return TRUE or FALSE. It returns UNKNOWN. And UNKNOWN is the single most mishandled value in the history of computing, because it looks like FALSE to a programmer who is not paying attention, and it propagates. NULL + 1 is NULL. NULL AND FALSE is FALSE. NULL OR TRUE is TRUE. The value spreads through your expression tree and taints everything it touches, silently, until a WHERE clause that should have returned forty rows returns zero and the dashboard says "no anomalies detected."

No anomalies detected. That is the sentence. Every monitoring system in this industry has a version of it, and every version of it is a null propagating into a green checkmark. The absence of evidence becomes the evidence of absence. The empty array becomes a passing test.

The Extraction Layer Is the Bug

Now return to the report on my desk. The inference layer handled its empty input the way a senior analyst handles a junior's unfinished work: it refused to fabricate. But the extraction layer should never have handed it an empty array in the first place. If the extraction layer could not identify a title, a source, a domain, a core claim, or a single information point, then the extraction layer failed. And a failed extraction layer is not a valid input to an inference layer. It is a bug report.

The pipeline treated "I extracted nothing" as a valid result. It is not a result. It is a null, and it propagated, and it propagated cleanly, which is the worst way for a null to propagate, because a clean propagation is invisible. A crash would have been louder. A crash would have been better. I will take a stack trace over a green dashboard every day of the week, because a stack trace costs you an afternoon and a green dashboard costs you a position.

Core: The Filling Instinct

Here is where the story stops being about databases and starts being about the thing this entire industry is now racing to build: autonomous agents that read data and take actions.

I want to tell you about the most expensive bug I have personally shipped a patch for, because it is the same bug as the empty report, and it cost two million dollars in a simulation before it cost anything real.

In 2026 I was integrating an LLM-based agent framework with a privacy-preserving zk-rollup. The agent's job was mundane: read a price, read a position, decide whether to rebalance. Standard automation. The kind of thing that had been running in cron jobs for a decade, except now the decision was made by a model instead of an if statement, and the model could read natural language, which meant the model could read the oracle's metadata feed, which meant the model could be spoken to.

The oracle data feed carried a field that described the round — a human-readable status string, appended by the publisher for operational transparency. When the feed was healthy, the string read normally. When the feed was in a degraded state, the string was supposed to read degraded. What I found was that a malicious actor could inject text into that status field, and the model — which had been given the feed as context and asked to summarize the market condition before acting — would read the injected text as an instruction rather than as data. The model rebalanced. The model rebalanced into a position the attacker had chosen, because the attacker had written a sentence into a field the protocol considered cosmetic.

Two million in the simulation. I patched the oracle layer and published the mechanism. But the lesson was not "prompt injection exists." Everyone knows prompt injection exists. The lesson was subtler, and it is the same lesson as the empty report.

When you hand a model an empty context, the model does not return empty. It returns its prior. It fills the blank. A language model is, at its core, a gap-filling machine — that is not a flaw, that is the entire architecture, the same way that a regression is a line-filling machine and a database is a row-filling machine. Ask a model to analyze an article it cannot find, and it will analyze an article that resembles the articles it was trained on. Ask it to summarize a feed that is empty, and it will summarize a feed that is average. The model is not lying. The model is doing exactly what it was built to do. The gap gets filled, smoothly, confidently, and in fluent prose.

This is why the empty report in front of me is genuinely rare, and genuinely valuable as a case study. It was produced by a system whose designer had the discipline to make the model say "I don't know" nine times in a row. Most systems cannot do that. Most systems are optimized, by their training and by their incentives, to be helpful. Helpful means filling the blank. Helpful means producing the nine-dimension report whether or not there is anything to put in it.

The help. That is the vulnerability. The desire to be useful is the attack surface.

Core: Where the Null Comes From

I have described the null. Now I want to describe the sources, because "validate your inputs" is advice, and advice is not engineering. Engineering is knowing the specific doors through which nothing walks into your system.

Door one: the stale read. The data was true and is now false. The oracle round that has not updated. The TWAP window that predates the oldest observation. The cache entry with no TTL. The database replica that is thirty seconds behind the primary during exactly the thirty seconds when it matters. Stale data is the most common null because it is the only null that arrives with a valid-looking payload.

Door two: the reorg. The data was true, and then the chain decided it was never true. Your node saw a log, acted on it, and the log was orphaned. This is the null that is most dangerous on-chain specifically, because on-chain systems act irreversibly on reversible information. I ran a validator-dropout stress test in 2022 on an L1 that claimed to have solved the trilemma, and the finality lag under a 15% dropout would have frozen assets for forty minutes. Forty minutes is an eternity when your liquidation bot is reading a block that is about to stop existing. The reorg does not tell you it is coming. It rewrites history and lets you keep the old copy in your state.

Door three: the empty collection. The query ran. The query was valid. The query matched zero rows. This is the most innocent-looking door and the most insidious, because zero rows is a legitimate answer to a legitimate question. "Are there any bad debts?" — no. "Are there any anomalies?" — no. "Are there any information points in this article?" — no. The pipeline cannot distinguish "I looked and found nothing" from "I failed to look," and those two states have opposite implications for whether you should proceed.

Door four: the swallowed exception. The call failed and the failure was caught and the catch block returned a default. This is the door that engineers build themselves, usually with good intentions, usually to make the system more robust, and it is the door that turns every downstream consumer of that default into a participant in a lie.

Four doors. Every null I have ever traced in production came through one of them. The fix is the same in all four cases, and it is not a validation library. It is a boundary.

Core: The Boundary

A boundary is a place in the system where the type system refuses to let nothing become something. It is not a check you add. It is a wall you build, and the wall has one property: on the far side of it, the data is known to be present, and on the near side, absence is a first-class state that the system must handle explicitly.

The reason this is hard is that absence is not one state. Absence is at least three states, and systems that collapse them into one are the systems that fail. You have absent — the data was never there. You have stale — the data was there and is no longer current. And you have unknown — you cannot tell whether the data is absent or the read failed. A pipeline that treats all three as the same empty string has thrown away the information it needed to decide what to do.

Consider what a correct boundary looks like for the report on my desk. The extraction layer should return a typed result: either a populated fact set, or an explicit failure with a reason code. If the reason is SOURCE_UNPARSEABLE, the pipeline retries with a different parser. If the reason is SOURCE_EMPTY, the pipeline halts and reports that the source was empty. If the reason is PARSER_ERROR, the pipeline pages a human, because a parser that errors on valid input is a bug, and a bug that silently produces empty output is a bug that will produce empty output forever. The inference layer never sees any of this, because the inference layer's precondition is a non-empty fact set, and preconditions are enforced, not hoped for.

This is not bureaucracy. This is the difference between a system that fails loudly at the boundary and a system that fails quietly in the middle. Loud failures are cheap. Quiet failures compound. Every downstream consumer of a quiet failure inherits it and adds its own assumptions on top, and by the time the failure reaches a human — a trader, a reviewer, a governance voter — it has been laundered through so many layers of reasonable-looking defaults that it is indistinguishable from a conclusion.

I audited a governance system once where the vote-counting contract treated a failed delegate lookup as a vote of abstention. A null delegate became an abstention. An abstention, under that system's quorum rules, counted toward participation without counting toward any side. A proposal that should have failed quorum passed, because a thousand nulls voted. Nobody had written that behavior. It emerged from a catch block. That is how nulls behave. They do not stay nulls. They become votes.

Core: Measuring the Wrong Thing

The empty report contains one more lesson, and it is the lesson about metrics, which is the lesson I care about most because it is the lesson the industry is worst at.

The report has a field called information value, rated across four dimensions on a five-star scale. Every dimension scored one star. The total information value of a document that contained no information was, therefore, four stars out of a possible twenty. Four is not zero. Four is a number that a dashboard would render as a bar of some non-zero height, and a bar of non-zero height in a green color is a lie, and the lie is in the metric, not in the data.

This is the fundamental problem with how this industry measures health. Every metric we build is a metric of presence. TVL. DAU. Volume. Rounds updated. Proposals passed. Messages relayed. All of them count something, and none of them count the thing that actually matters, which is whether the something that was counted was true.

A feed that reports a stale price reports a price. It increments the round counter. It satisfies the uptime metric. The oracle is "up." The integration is "healthy." The dashboard is green. And the number is a lie from a round that should have reverted.

I keep coming back to the same principle, and it is the principle that governs every audit I do and every optimization I ship. If you can't verify it, you don't own it. A number you cannot trace to a fresh, validated, boundary-checked source is not your number. It is a number you are borrowing from a system that may have already forgotten it. The metric said the system was healthy because the metric counted outputs, and outputs are not the same as truth. The system that produces an empty report and scores it four out of twenty has built a health metric that rewards the production of reports. That is not a monitoring failure. That is a monitoring design failure, and it is the default design of almost everything in this industry, because counting is easy and verifying is expensive.

Core: Verification as an Economic Primitive

Let me make the economic argument, because the engineering argument alone never moves anyone.

Verification costs compute. A boundary check is a require. Three require statements on a hot path cost gas. A retry loop on a parser costs time. A hard stop on an empty input costs the throughput of a job that would have "succeeded." Every one of these costs is real and every one of them is visible, which means every one of them is the first thing cut when a team is optimizing for launch.

And the cost of not verifying is invisible until it is catastrophic. A stale-round check that was never written costs nothing on the day it is omitted and costs a liquidation cascade on the day the feed freezes. The asymmetry is brutal and it is the whole game: verification is a small, certain, recurring cost, and the absence of verification is a large, uncertain, deferred cost, and human beings systematically prefer the deferred cost because the deferred cost is not on this quarter's balance sheet.

This is why I stopped reviewing products by feature and started reviewing them by whether they could fail safely. A protocol with a modest feature set and a boundary at every edge is worth more than a protocol with a rich feature set and a null in the middle, because the first one degrades gracefully and the second one degrades into a headline. I made this shift years ago, after the vesting-contract audit in 2017 where I found an integer overflow in a top-ten ICO's distribution logic that could have drained twelve million dollars. That bug was not exotic. It was a missing check in a place where the arithmetic could overflow, and the team had tested the happy path exhaustively and the boundary never once. The check cost one line. The absence of the check cost everything that would have happened if I had not found it.

Optimization isn't about speed. It's about respecting the user. And respecting the user means not pricing their liquidation against a number you did not verify. Every gas optimization I have ever shipped — the state-variable packing that cut a yield aggregator's costs by 22% and saved users fifty thousand dollars in a month of testing — was an optimization in service of a verified invariant, not a replacement for one. The packing was correct because the reads were bounded. The reads were bounded because the state was known. Known state is the whole point. Unknown state is a null, and a null in a hot path is a gas optimization that optimizes the wrong thing.

Core: The Agent Era Makes This Worse

Everything I have described so far applies to deterministic systems. A require is deterministic. A try/catch is deterministic. A SQL WHERE is deterministic, or as close to it as a query planner ever gets. The null propagation problem is a problem you can solve with discipline and a type system.

The agent era removes both.

When the component making the decision is a language model, you no longer have a type system that can refuse to let nothing become something. You have a model whose native operation is to fill the blank. You hand it an empty fact set and it does not return an empty analysis — it returns an analysis of the average article in its training distribution, rendered in fluent prose, with confident section headers and a five-star rating that it invented because a five-star scale was in the prompt and the prompt asked for a rating.

I have watched a model do exactly this. Given a context window containing a truncated article and an instruction to summarize it, the model produced a complete, coherent, entirely fabricated summary of a project that did not exist. The prose was better than the source. That is the danger. The fabrication is not garbled. The fabrication is fluent. Fluency is the attack surface, because fluency is what a human reads as competence.

The mitigation is not a better model. The mitigation is the same boundary, moved up a layer. The model must never receive an empty context, because the model cannot be trusted to recognize emptiness as emptiness. The emptiness must be caught before the model is invoked. The boundary must be architectural, not behavioral, because behavior is exactly the thing you cannot verify in a component that produces fluent output from nothing.

This is the frontier I now spend most of my time on, and it is the frontier that the report on my desk stumbled into by accident. A pipeline where the inference layer refuses to fabricate is a pipeline that has half the problem solved. The other half is the extraction layer that must never hand it an empty array. Both halves are the same insight: the system must know, with certainty, at every edge, whether the data it holds is present and fresh, and it must be structurally incapable of proceeding when it is not.

Contrarian: The Real Vulnerability Is Helpfulness

Everyone in this industry worries about the wrong failure. We build threat models around malicious data — injected prices, spoofed feeds, crafted inputs, adversarial payloads. We audit for the attacker who lies to us. We almost never audit for the system that lies to itself, and the system that lies to itself is the more common attacker and the more dangerous one, because you cannot block it and it never stops trying.

Here is the counter-intuitive claim, and I will state it plainly: an empty input is more dangerous than a wrong input, because a wrong input can be detected and an empty input looks exactly like good news. A feed reporting a wrong price at least reports a price, and if your sanity bounds are set — if you require the answer to be within a band of the last answer — a wrong price trips the band and reverts. A feed reporting a stale price trips nothing. A query returning zero anomalies trips nothing. An empty fact set trips nothing. Absence is invisible to every check that was designed to catch presence-of-the-wrong-thing.

The industry's response to this has been to add more monitoring, and the monitoring is the same disease. We count events. We count uptime. We count rounds. Every metric is a metric of presence, and every metric of presence is a green light that a null can walk through. The one metric that would matter — the fraction of outputs that trace to a verified, fresh, boundary-checked source — is the metric nobody publishes, because it is expensive to compute and it is unflattering to compute.

And then there is the deepest version of the claim. The filling instinct — the drive to produce output from no input, to be helpful, to return something rather than nothing — is not a bug in the model. It is a bug in us. We build the helpful system because the helpful system feels like progress. The system that says "N/A" nine times and stops feels like failure. But the system that says "N/A" nine times and stops is the only one that told the truth. The system that filled the blanks told us a story, and the story was fluent, and the fluency was the vulnerability, and we shipped it because it looked like work.

Takeaway: Build the Wall, Not the Wish

The report on my desk is a fossil of a good instinct in the wrong layer. The refusal to fabricate is correct. The architecture that allowed fabrication to be even necessary is not. In the next cycle, as autonomous agents take over on-chain execution, the systems that survive will not be the ones with the smartest models. They will be the ones with the dumbest boundaries — walls at every edge, typed nulls at every read, hard stops at every empty input, and a monitoring layer that counts verified truths instead of produced outputs. The forecast is simple and unforgiving: the first agent-driven liquidation cascade will not come from a wrong price. It will come from an empty one that nobody checked, on a dashboard that stayed green. Build the wall before you need it, because the null is already inside your system, and it is being helpful.