Null Propagation in On-Chain Analytics: When Missing Data Renders as 'No Risk'

Neotoshi In-depth

Last week I ran a nine-dimension due-diligence framework against a research payload. The payload passed schema validation. Exit code zero. The pipeline logged success.

Every field came back N/A.

Not one field was wrong. There was nothing in the payload for it to be wrong about. The title key was empty. The source key was empty. There were zero information points — zero — inside a framework whose entire architecture assumes information points exist. Nine dimensions, roughly ninety sub-metrics, all of them returning the same grey string in the same neutral typeface.

The system did not fail. That is the failure.

A parser that throws is a parser you can fix. A parser that emits a structurally valid document containing no claims is a parser that lies by omission, and it lies in the precise visual register your dashboard reserves for "nothing to worry about." I've spent nine years reading smart contracts, price feeds, and bridge state hunting this bug class. It always has the same shape: the absence of a signal is rendered as the presence of safety. Code does not lie, but it does hide.

The framework was never supposed to be clever. It was supposed to be boring. Nine passes over a protocol — technical architecture, token supply and unlock schedule, market structure, ecosystem position, regulatory exposure, team and governance, risk matrix, narrative-versus-delivery, and supply-chain transmission. Each pass emits findings with a confidence tag. Low-confidence findings stay in the document and get flagged. High-confidence findings get cited. Findings with no source get rejected at write time.

That last rule is the one that broke.

Null Propagation in On-Chain Analytics: When Missing Data Renders as 'No Risk'

When the input payload arrives empty, there is nothing to reject. The rejection rule fires on claims without sources. It does not fire on the absence of claims. So the template renders. Every cell fills. The string "N/A" is not a claim, so it passes validation. The document ships. Somewhere downstream, a human reads nine pages of grey and concludes the protocol is unremarkable rather than unexamined.

Null Propagation in On-Chain Analytics: When Missing Data Renders as 'No Risk'

Here is the mechanical reason, and it is worth sitting with because it generalizes far beyond my own tooling. Schema validation is not semantic validation. JSON Schema's required keyword checks whether a key is present. It does not check whether the value behind that key carries information. {"title": ""} satisfies required: ["title"]. It fails minLength: 1, which is the constraint nobody writes, because writing it feels pedantic until the day it isn't.

Anyone who has run an oracle integration has met the same bug from the other side. Chainlink's latestRoundData() returns five values: roundId, answer, startedAt, updatedAt, answeredInRound. The answer is the price. The other four exist because the price alone is not information — the price plus its timestamp is information. A round that answered forty minutes ago is not a price. It is a memory of a price.

Null Propagation in On-Chain Analytics: When Missing Data Renders as 'No Risk'

Half the forks I have reviewed in the last three years check answer > 0 and nothing else. They treat a stale round as a live one. The integration passes every test environment, because in test environments the oracle answers immediately and never lies by delay.

Missing data and null data are different objects, and almost nobody's code distinguishes them. That is where we are.

In JavaScript, null, undefined, 0, "", NaN, and false all fail if (!x). In Python, None, 0, 0.0, "", [], {}, and False all fail if not x. These are convenient defaults and they are the single most common source of silent data corruption I have found in production crypto tooling.

Consider a wallet balance query. The RPC returns "0x0". Your formatter parses it to 0. Your guard clause is if (!balance) return "—";. The user sees an em dash. The user believes the query is pending. The user retries. The retry also returns 0. The user now believes they hold nothing, and opens a support ticket, and the support ticket closes with "shown as zero because it is zero."

Now flip it. Consider a liquidation keeper that only fires when lastPrice is truthy. A price feed that genuinely returns zero for one block — during a settlement failure, a depeg, a round with no answer — reads to that keeper as "no data, skip this tick." The keeper skips. The position is not liquidated. The protocol eats the bad debt, and the postmortem says the keeper was buggy, and the keeper's author says the feed returned nothing, and both are right.

I watched this pattern compound in March 2020. On the 12th, ETH lost roughly 40% inside a day. The MakerDAO liquidation system produced zero-bid auctions — on the order of $8.3 million of collateral moved to keepers who bid nothing DAI, because gas congestion meant the only transactions capable of landing were the ones that did not need to win an auction. Keepers holding correct prices could not get included. Keepers who got included had no fresh prices.

The postmortem focused on the auction mechanism. The mechanism was fine. The mechanism was fed by keepers whose inputs were delayed, and a delayed input inside a ten-minute auction window is operationally indistinguishable from a missing input.

The fix in oracle-land is four lines:

(uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = feed.latestRoundData();
require(answeredInRound >= roundId, "stale round");
require(updatedAt != 0, "round not complete");
require(block.timestamp - updatedAt < MAX_DELAY, "stale price");

I have pasted those four lines into more reviews than I can count. They are not original. They are not clever. They are the difference between a protocol that survives a feed outage and a protocol that discovers it had one three hours later.

In 2020 I ran a bot against Curve's StableSwap pools with about $15,000 of my own capital behind it. The goal was not profit. The goal was to map the invariant function — the Newton's method solve in get_D, the fee accumulation, the A-parameter ramp — and find where the pool's internal accounting diverged from what the chain would actually settle. I found a window, a few blocks wide, where the pool's state and the chain's state disagreed. The bot read the true state, acted on it, and the interface kept showing the old number to everyone else.

That is the entire game. The edge is not information. The edge is the delta between when information becomes true and when the interface admits it.

Layer 2s hand you a second version of exactly this. A rollup sequencer produces blocks on a schedule independent of L1. That is the point — cheap, fast confirmation. But "sequencer-confirmed" and "L1-verified state" are two different claims carrying two different timestamps, and only one of them is the truth a bridge can act on.

When the batch submitter lags — I have watched this happen during L1 gas spikes, during every stress event I would bother naming — the chain keeps producing blocks. The RPC keeps answering. Balances update. The DEX keeps swapping. From the user's seat, everything is live.

From the bridge's seat, the state has not been posted to L1. Nothing is confirmed. The withdrawal queue is not stalled because the sequencer went down. It is stalled because the data is not where the verifier can see it. Two clients, two answers, both defensible.

A sequencer does not have to be offline to hand you a stale read — it only has to be behind. I have argued for two years that decentralized sequencing is a slide deck, and here is the technical form of that argument: if the sequencing function and the batch submission function run on infrastructure you do not control, you are not reading a chain. You are reading a company's uptime report. Redundancy is the enemy of scalability, and the sequencer is the redundancy we all quietly agreed to delete.

In 2021, during the NFT peak, I ignored floor prices for a weekend and audited the metadata layer of the top ten collections. I pulled the tokenURI for each token, resolved the pointer, and checked where the bytes actually lived.

Roughly 40% of the "decentralized" collections in that set had at minimum one of: a centralized HTTP gateway in the tokenURI, a DNS name controlled by a single entity, or a pinning arrangement with no redundancy. The content addressed fine. The addressing was the problem.

Here is the part that matters. When one of those gateways stopped answering, marketplaces did not render a broken NFT. They rendered a placeholder. A grey square. A default avatar. Something that looks like it is loading.

Absence was rendered as decoration. Nobody panicked, because the failure had been styled to resemble latency. I took a consulting role with a gallery off the back of that audit, and the first thing I told them was that they needed a persistence budget — an actual line item for keeping bytes alive — because ownership of a token is a claim on a pointer, and pointers rot.

During the 2022 drawdown I spent three weeks doing opcode-level profiling on a Layer 2 rollup's contract set. Not reviewing it. Profiling it. I found an 18% reduction in average transaction cost, and it came from boring places: cold SLOADs that could have been cached, storage slots that could have been packed, calldata that could have been read instead of copied into memory. Post-EIP-2929, a cold SLOAD runs 2,100 gas. Multiply that by the call paths users actually take and the savings stop being cosmetic.

I shipped it behind a 500-transaction canary. Eighteen percent off every transaction is, in a bear market, the difference between a protocol people keep using and one they quietly stop.

Here is why that story belongs in this article. Not one of those inefficiencies had ever triggered an alert. The transactions succeeded. They were merely expensive. Nothing in the monitoring stack was built to catch a system that was working — just badly. We instrument for failure. We do not instrument for absence, and absence is the cheaper, more common, more survivable-looking failure mode.

In 2024 I co-designed a zero-knowledge verification layer for an ETF provider's internal compliance tooling. The requirement was narrow: prove that a set of addresses satisfied a sanctions screen without revealing the addresses. We ran 10,000 simulated transactions through it before it touched anything real.

The failure mode we spent the most engineering time on was neither a false positive nor a false negative. It was a timeout. If the screening service did not respond, what should the proof layer emit? "Not screened" and "screened, clear" are different states. "Screened, indeterminate" is a third. A compliance system that collapses that third state into the second one is not a compliance system — it is a rubber stamp with a hash function. Logic gates are the new legal contracts, and most of them ship with a default branch that returns true.

Now the counterintuitive part, and it argues against nearly every dashboard I have reviewed this year.

The industry spends heavily on preventing wrong answers. Circuit breakers. Reorg detection. Price deviation guards. Redundant feeds. All of it engineered for the case where the data says something false.

It spends almost nothing on preventing absent answers. Not because absence is rare — absence is constant. It is because absence is cheap to render and expensive to represent.

To display a wrong price you need a comparison. To display an absent price you need a state machine. A grey "N/A" costs one string. A red "DATA UNAVAILABLE AS OF BLOCK 21,447,102 — LAST GOOD PRICE 41 MINUTES AGO — POSITIONS USING THIS FEED ARE UNSAFE" costs a design review, a copy pass, and an argument with someone who believes red increases support load.

So we ship the grey string. The user sees a dash. The dash is neutral. Neutral is the color of "we checked and it is fine." The user's mental model of an empty field is no problem detected. The engineer's mental model of an empty field is the request did not return. Those two models sit in the same interface, three pixels apart, and the distance between them is the distance between a position that gets liquidated and one that does not.

I have watched this misread cost real money four times that I can document from the inside. Every time, the postmortem landed on the protocol. Every time, the actual failure was in the rendering layer — the place where a system decides what a blank means. Tracing the noise floor to find the alpha signal only works if you know when the signal is missing rather than merely quiet.

The next version of this problem is already shipped and it is worse. Research pipelines, monitoring bots, and now language-model ingest layers all consume the same three shapes: a schema, a payload, and a default branch. When the payload is empty, the default branch is what you get, and the default branch is always the cheapest thing a designer could render. A model summarizing an empty document will summarize it confidently.

I have written the fix into exactly one production system in nine years, and it was the ETF tooling. The rule was one sentence: no null reaches a human without a label. Not "N/A." A label that states what is missing, when it was last present, and which positions depend on it.

The question worth sitting with is not whether your pipeline handles empty input. Every pipeline handles empty input. The question is whether your pipeline would tell you it did.