The Null Oracle Problem: What Blockchain Data Pipelines Do When the Answer Is N/A

CryptoStack
Wallets

The Null Oracle Problem: What Blockchain Data Pipelines Do When the Answer Is N/A

On May 12, 2022, the LUNA/USD price feed on BNB Chain was still returning $0.10.

LUNA was trading in fractions of a cent. The feed did not revert. It did not return zero. It returned a well-formed, correctly signed integer, with a fresh timestamp and a valid round identifier, stating that one LUNA was worth ten cents. Every sanity check a careful integrator would have written β€” answer greater than zero, updatedAt inside the heartbeat window, answeredInRound at least equal to roundId β€” passed. All of them passed, for hours, while the market underneath was already a crater.

I keep returning to that feed because it is the cleanest specimen of a failure class almost nobody audits: the data source that keeps answering a question it can no longer answer, in exactly the format its consumers expect. Chainlink exposes per-feed floor and ceiling parameters. The stated intent is defensive β€” stop a feed from printing nonsense during a violent move. The realized effect on that day was a feed pinned roughly four orders of magnitude away from reality while reporting, in every structural sense, perfect health. Venus Protocol on BNB Chain accumulated roughly $11.5 million in bad debt to borrowers who deposited LUNA as collateral at the pinned price and withdrew stablecoins against it. No one exploited a bug. They read the feed correctly. The feed lied politely, and politeness is worse than an exception.

Context: a price feed is a policy, not a thermometer

A thermometer reports a physical quantity that exists whether or not anyone measures it. A price feed reports an aggregation of discrete, adversarial, timestamped observations passed through a policy engine that decides what to publish, when to publish it, and what to do when the inputs become unusable. The policy layer is where the losses live. It is also the layer that receives the least audit attention, because it is documented as configuration rather than as logic, and configuration is assumed to be somebody else's problem.

How an aggregator round actually works

Push oracles, Chainlink being the canonical example, run off-chain infrastructure that watches a set of venues, computes a median or a volume-weighted aggregate, and writes into an on-chain aggregator contract when a trigger fires. Two triggers matter. The deviation threshold, expressed in basis points, fires when the aggregate moves enough to be worth publishing. The heartbeat, expressed in seconds, fires when the aggregate has not moved at all. The heartbeat exists so that consumers reading updatedAt see a number that is recent even during calm.

That design choice is where the first lie enters the system. A heartbeat update proves the oracle is alive. It does not prove the price changed, and it certainly does not prove the price is tradeable. A feed can tick every twenty minutes through an entire weekend while the underlying market is closed, halted, or sitting on order books with three hundred dollars of depth on each side. Every consumer that treats freshness as a proxy for validity will act on a re-stamped number that means nothing, and will do so with the full confidence of a contract that just passed its staleness check.

The second structural trap is the round itself. An aggregator round is a bundle of five fields: roundId, answer, startedAt, updatedAt, and answeredInRound. The last field is the one that trips people. answeredInRound records the round in which the current answer was actually computed, and when a new round opens and has not yet been answered, answeredInRound on the live round points backward to the previous one. Consumers that compare answeredInRound against roundId are checking whether the value they are reading was finalized in a complete round, which is a genuinely different question from whether it is fresh. Consumers that skip the check can read answeredInRound from an open round that still carries the previous answer β€” a value that is simultaneously current and outdated, depending on which field you trust. Most integrations do not know which field they are trusting. They copy a require statement from a blog post and move on.

The evolution of the staleness check

The industry did learn something. After 2022, updatedAt guards became standard in any contract holding meaningful value. That is real progress and I do not want to understate it. But progress in one dimension tends to relocate risk rather than retire it, and the current distribution of risk is instructive.

Here is a rough census from the integrations I have reviewed over eighteen months. Nearly all of them check answer greater than zero. Most check updatedAt against block.timestamp. About half check answeredInRound against roundId. A small minority check that the feed address is the one they think it is, rather than whatever was passed in at construction. Essentially none of them check the floor. Essentially none of them check the ceiling. Essentially none of them read the aggregator implementation at all.

The floor is the parameter that matters most in a crash, and it is the parameter least exposed to consumers. It lives in the underlying off-chain aggregator contract, not in the facade interface that everyone imports. To see it you have to call the specific implementation, or query an external registry, or hardcode a value you found by reading a deployment transaction. Every one of those is friction, and friction in a security check is indistinguishable from absence.

Two designs, one blind spot

Pull oracles and on-chain TWAPs invert the push model. Uniswap V3 does not push anything. It accumulates tick observations into a ring buffer inside the pool contract, and consumers call observe with an array of secondsAgo offsets to reconstruct a time-weighted average price. It is elegant, self-funded, and manipulation-resistant in exact proportion to how long the window is and how deep the pool is.

That conditional clause is doing enormous work, and the parameter that governs it is called observationCardinality β€” the number of slots actually allocated in the ring buffer. A freshly initialized pool has cardinality of one. It contains exactly one observation, written at initialization. Ask that pool for a thirty-minute TWAP and it will not revert. It will not return zero. It will return a plausible number that is mathematically the pool's initial tick, reweighted across a window during which the pool recorded nothing. The result is not stale in the way a Chainlink feed is stale. It is not derived from any market input. It is a clean arithmetic artifact of an empty buffer.

Over the past seven days my own monitoring stack has flagged four protocols whose primary collateral feed spent more than 15 percent of its updates above the heartbeat threshold with no deviation movement. That is not a bug in any of them. It is a configuration state, and configuration states are where bear markets find their victims. When I ran the same scan against the pool set those protocols use as secondary references, three of the pools had observationCardinality sitting at a value that would make a thirty-minute TWAP return a single stale tick.

Both designs share a structural property the industry has never fully internalized. When an oracle has nothing to say, it does not say nothing. It says something. That something was chosen by protocol designers months or years earlier, typically during a design review, typically framed as a safety parameter, and almost never documented as a normative claim about what the published number means when it is wrong.

Core: the anatomy of a null return

I want to be precise about terminology, because sloppy language lets real bugs hide behind familiar words.

A data pipeline can fail in five ways, and only the first is what engineers usually mean when they say failure.

A hard null. The call reverts. The transaction rolls back. The consumer receives an exception and, if the code is written well, halts. This is the good failure. It is loud, atomic, and cheap to handle.

A stale return. The call succeeds and returns the last value, with a timestamp old enough that a check should catch it. This is the failure mode most audit checklists actually cover, which is why most integration checklists mention updatedAt and stop there.

A floored return. The call succeeds, the timestamp is fresh, and the value has been clamped by a parameter the consumer has never read. This is the Chainlink LUNA case. This is the case that killed Venus.

A malformed return. The call succeeds and returns a value in a shape the consumer misinterprets β€” a decimals mismatch, a signed integer read as unsigned, a scaled value read unscaled, an array read in the wrong order.

A correctly computed, honestly reported, wrong answer. Every layer functioned as designed and the aggregate itself was poisoned, because the inputs came from venues the adversary controlled.

Only two of those five produce anything a traditional error handler would notice. The other three produce numbers. That asymmetry is the entire problem, and it is why I have stopped describing oracle risk as a question of data availability. The data is available. The data is wrong.

The Solidity that swallows the null

Here is a pattern I have found in roughly a dozen production codebases over the past two years, in slightly different costumes each time.

function getPrice(address feed) internal view returns (uint256) {
    (, bytes memory data) = feed.staticcall(
        abi.encodeWithSignature("latestRoundData()")
    );
    (, int256 answer, , , ) = abi.decode(
        data,
        (uint80, int256, uint256, uint256, uint80)
    );
    return uint256(answer);
}

Read the first line again. staticcall returns a success boolean. The code names it to nothing β€” a bare comma β€” and discards it. If the feed reverts, data is empty, and abi.decode on empty bytes will revert in its own right, so the immediate damage is contained by accident rather than design. Now consider the variant where the decode does not revert because the caller pre-populated data from a previous read, or where the call routes through a proxy that returns a hardcoded fallback. In those cases the success flag was the only signal that anything had gone wrong, and it was thrown away at the point of definition. I have seen this exact structure survive two audits, because both auditors read the decode line and neither read the comma.

The sloppier and far more common version is this one.

(uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound)
    = feed.latestRoundData();
require(answer > 0, "bad price");

Three checks are missing and one is subtly wrong. answeredInRound greater than or equal to roundId is absent, which means a round superseded mid-flight can still be consumed. updatedAt is captured into a variable and never compared against block.timestamp, which means the staleness guard exists as a declaration and not as a guard β€” a pattern I have started calling decorative security, because it reads as diligence in review and executes as nothing. And answer greater than zero treats positivity as validity, which is precisely the assumption a floored feed violates: the LUNA feed was returning 10000000 for a value that should have been 1 or 0 at six decimals. Positive. Fresh. Wrong.

I have written this check enough times that I now write it once, as an imported function, and never inline.

function readFeed(
    AggregatorV3Interface feed,
    uint256 maxStaleness
) internal view returns (int256) {
    (uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound)
        = feed.latestRoundData();
    require(updatedAt != 0, "round not complete");
    require(answeredInRound >= roundId, "stale round");
    require(block.timestamp - updatedAt <= maxStaleness, "stale price");
    require(answer > 0, "non-positive price");
    return answer;
}

Four lines of defense, and here is the uncomfortable part. That function still does not protect you against a floored feed, because a floored feed passes all four checks. The floor is above zero. The floor carries a fresh timestamp. The round is current and answered. The only defense against a floor is knowing the floor exists and encoding a second condition β€” that the answer is not exactly equal to the configured minimum β€” and the minimum is not exposed by the facade interface at all.

I have yet to review a production lending market that does this. Not one. Math does not negotiate, and neither does a parameter nobody reads.

Anchor Protocol, and a correction I owe myself

In 2021 I spent three weeks reading Anchor Protocol's CosmWasm contracts after the UST depeg began. I published a fifteen-page post-mortem at the time in which I described the amplifying mechanism as an integer overflow in the redemption path. I want to correct that here, because the reputation of a post-mortem matters less than the accuracy of the mechanism it describes, and I have had four years to reread the Rust.

It was not an overflow. Anchor's money-market contract tracked a share token, aUST, against an exchange rate that grew with accrued interest. The rate was computed by dividing total deposited value by total aUST supply, using a fixed-point decimal type carried at eighteen digits while the underlying UST amounts were carried at six. Every deposit, every withdrawal, every interest accrual round-tripped between those two precisions. Truncation in the round trip was lossy in a consistent direction, and the error compounded along the withdrawal path.

What made that truncation fatal rather than merely annoying was ordering. The withdraw path computed the payout using the current exchange rate, burned aUST, and then updated the totals from which the next rate would be derived. Under normal load the ordering is invisible. Under the load of a depeg β€” thousands of withdrawals per block, each consuming slightly less than it removed, each leaving the rate a fraction higher than it should be β€” the rate drifted upward while the reserve drained. A higher exchange rate means aUST holders believe their shares are worth more, which means the marginal withdrawal costs the protocol more than the deposit that funded it. That is an amplifier wired directly into the accounting layer, and it fed into the death spiral from the inside.

The distinction matters because it changes the mitigation. An overflow is fixed with a bounds check. Precision drift across an ordering boundary is fixed by choosing a canonical precision, performing the arithmetic in integers at that precision end to end, and defining the rounding direction explicitly for every operation that touches the rate. Anchor did none of those things. The audit reports I have read from that period discussed the decimal types without ever asking what happens when the same account structure is touched ten thousand times in a hundred seconds.

Code is law, but bugs are reality, and the bugs that kill protocols are rarely the bugs the audit checklist was written for. A checklist written on a Tuesday afternoon in a calm market is a description of Tuesday afternoon in a calm market, and it will be applied on a Thursday in a panic.

The oracle that reports its own birthday

Return to the V3 observation buffer, because it is the most under-audited piece of oracle infrastructure in production and because it fails in a way that looks exactly like success.

When a pool is created, slot0 is written with the initial tick and an observation is pushed into slot zero of the oracle array. observationCardinality is one and observationCardinalityNext is one. Most integrations read slot0 directly, which is the spot price and is trivially manipulable inside a single block by a flash loan. Serious integrations call observe.

Now consider what observe returns with offsets of 1800 and 0 against that pool. The function walks backward through the observation array, finds a single entry, and interpolates. There is nothing to interpolate against except the initial observation, so the result is the initial tick held constant across the entire window. The function returns a number. The number has a defensible interpretation: it is the time-weighted average price over the last thirty minutes, weighted by the only observation that exists. It is also completely disconnected from the current market, and if the pool was initialized at a tick corresponding to a price that no longer holds, the integration is now pricing collateral off a memory.

The correct guard is to compare observationCardinality against the number of observations a window of the requested length requires, and to refuse the pool if the buffer is too shallow. Almost nobody does this. The parameter is a public view function. The check costs one extra call and four lines of code. And the failure it prevents is one of the few in this space that produces no error, no stale timestamp, and no floor. It just produces a number from a buffer that never had anything in it.

I have started calling this the birthday problem of oracle design. The oracle reports the state of the world at the moment it was born, forever, and it never lies about anything except the passage of time.

Bridges: when the null crosses chains

Everything above concerns a number that is wrong in place. Cross-chain messaging introduces a second axis, because now the null has to travel, and travel creates its own failure modes.

A bridge verifier is handed a claim about state on a remote chain and asked to certify it locally. The null-return analysis applies with more force here, because the verifier typically cannot independently observe the remote state at all. It sees whatever the attestation layer chose to publish. If the attestation is missing β€” because the remote chain halted, because the sequencing layer stalled, because the message was simply never gossiped β€” the local verifier is faced with the question that every consumer in this article has faced and answered badly. What do I do when the answer is not here?

The historical record is unambiguous about which answer gets chosen. Wormhole lost roughly $320 million in February 2022 to a signature verification failure that allowed a forged guardian set message to be accepted as a valid attestation. Nomad lost roughly $190 million in August 2022 to an initialization flaw that made an uninitialized trusted root acceptable, which meant that any message could be proven against an empty accumulator. Both were hard bugs rather than configuration drift, but both share a shape with the floored feed: a verification path accepted a value because a check that should have rejected an unauthenticated state was never reached, or reached with the wrong operand.

The general lesson is that cross-chain verification does not eliminate the null. It relocates it to whatever layer is responsible for deciding what a missing message means, and then lets the outcome propagate with the finality of a settled transaction. There is no revert that undoes a message that was never supposed to be accepted.

The one place absence is proven

There is a corner of this industry where the null is handled correctly, and it is worth studying precisely because it is the exception.

Zero-knowledge systems have a primitive called the nullifier. In a shielded transfer, the sender proves in zero knowledge that a note β€” a commitment hiding an amount, an owner, and a secret β€” exists in the set of all valid notes, and publishes a nullifier derived deterministically from that note's secret. The contract maintains a set of spent nullifiers. If the nullifier is already present, the transaction is rejected. If it is absent, the transaction proceeds and the nullifier is inserted.

The nullifier is a value whose entire purpose is to prove that something does not exist. It is the cryptographic formalization of absence that every other oracle in this industry fudges. The design is instructive because it makes the absence explicit, verifiable, and external to the party asserting it. The sender does not claim a note is unspent; the sender publishes a value whose presence in a public set would contradict the claim, and the verifier checks the set. Absence is not assumed. Absence is proven, and the proof is cheap.

Compare that to the price feed model, where absence is expressed as a floor, a heartbeat re-stamp, or an interpolated zero-observation average. In every one of those cases the consumer is asked to infer absence from a number that was designed to be present. That inference fails, reliably, in exactly the conditions where the stakes are highest. Privacy is a feature, not a bug, and so is the honest reporting of missing data β€” but only the first one has a research community behind it, a conference track, and a funding line. The second one has a comment in a config file.

The N/A field and the compliance circuit

In 2025 I worked with a legal-tech team integrating zero-knowledge compliance proofs into a lending protocol. The requirement was straightforward on paper: verify that a borrower satisfies a creditworthiness predicate without revealing the underlying financial data. The interesting engineering was not the predicate. It was what to do when the private input was missing.

A compliance check has three outcomes, and naive circuit design collapses them into two. Satisfied. Not satisfied. And β€” the one that matters β€” the data required to evaluate the predicate does not exist. In a conventional system that third state is represented as a null, an empty field, an N/A. On chain, inside a circuit, every wire must carry a value. There is no null. There is zero, and zero is a valid credit score, a valid balance, a valid income.

We spent two weeks on that problem alone, and the solution we landed on was to carry an explicit validity flag as a public input alongside the predicate result, so that a witness with no data could not be silently coerced into a witnessed zero. Proof generation time fell from roughly 500 milliseconds to 150 after we restructured the constraint system, but the restructuring that mattered was the one that added a single wire.

The lesson generalizes far beyond compliance. Every system that encodes N/A as a default value has made a policy decision, and every system that does not document that decision has hidden it. The on-chain lending market has spent five years encoding N/A as zero, then writing require(price > 0), then wondering why the floored feed passed. Zero passed the check. Zero was never the problem. Zero was the disguise.

Verifying an answer when you cannot see the question

I have spent part of this year on a related problem: verifying the integrity of off-chain AI model outputs. The setup is familiar to anyone who has dealt with oracles. A model runs off chain, the result is posted on chain, and a contract needs assurance that the posted result actually came from the declared model applied to the declared data without tampering.

The verification story is tractable when the model is small and the input is known. It becomes structurally identical to the oracle problem when either of those conditions breaks. If the input dataset is not published, a verifier can establish that some model with some weights produced the output. It cannot establish that the model was the one claimed, or that the input was the one claimed. The proof is valid. The proof is also answering a different question β€” the same shape as a TWAP interpolated across an empty observation buffer: formally correct, contextually vacant.

My prototype approached this by committing to the model weights and the input dataset as public inputs, and having the circuit prove that the arithmetic from those commitments to the published output was performed correctly. That works. It also means the proof is a statement about a specific dataset that must be published and retained, which reintroduces a data availability dependency at the exact moment the industry has been learning to distrust them. Verifiable inference does not eliminate trust. It relocates it, and the relocation is only worth doing if the new location is auditable by someone who is not the operator.

What a null-return audit actually looks like

I want to make this concrete, because the argument so far has been structural and structure is easy to nod at and hard to apply. Here is the procedure I run now on any integration that reads external data, in the order I run it.

Enumerate every external read. Every feed address, every pool, every attestation endpoint, every cross-chain message source. If an address is passed in at construction rather than hardcoded, note it, because that is an upgradeable trust surface pretending to be a parameter.

For each read, write down the answer to a single question: if this source could not know the true value, what would it return, and would my checks catch it? For a Chainlink feed the answer is the floor or the ceiling or the last heartbeat value. For a V3 pool it is the initial tick. For a cross-chain attestation it is whatever the default message state is. For an NFT floor oracle it is the last sale price, indefinitely.

Then read the implementation. Not the interface. The specific deployed implementation behind the proxy, fetched from the chain and decompiled if necessary, because the floor lives in the implementation and the interface does not know about it.

Then check the arithmetic boundary. Where does the decimal precision change, and in which direction does it round, and does the ordering of operations let the rounding accumulate? This is the Anchor lesson and it is the one auditors skip most often, because it requires reading the same function three times with different assumptions about load.

Then check the guard clauses against an empty state. Not a wrong state. An empty one. Most guards are written to reject adversarial values, and an empty state is not adversarial. It is merely absent, and absence passes more checks than any adversary ever will.

The whole procedure takes a day for a simple integration and a week for a lending market. It has never produced zero findings. Not once.

Contrarian: the industry audits the happy path and calls it security

Here is the uncomfortable structural claim.

Security spending in this industry concentrates almost entirely on the adversarial case. Reentrancy. Price manipulation. Access control. Flash loan composability. An entire audit economy has grown up around one question: what can a hostile actor make this contract do? That question matters, and the answer is worth the money. But it crowds out a second question with almost no commercial sponsor β€” what does this contract do when its inputs are missing, and who decided the answer?

The reason no market exists for that second question is that it has no adversary. A null return is not an attacker. A floored feed is not an exploit chain. A pool with cardinality one is not malicious. There is nobody to blame and nobody to sue, which means there is no line item in a budget that covers it. The result is a security posture that is excellent against clever opponents and indifferent to dumb conditions. Bear markets are made of dumb conditions.

Consider what happened to Compound in November 2020, when the DAI price feed spiked to roughly $1.34 on a thin venue and cleared more than $90 million in liquidations from positions that were never actually at risk. No attacker. No exploit. A data source answered a question nobody had asked it, and the protocol acted on the answer because the answer was well-formed. Consider Cream Finance, which accepted yUSD as collateral at a value derived from a market with essentially no depth. Consider Mango Markets, where the adversary did not break the oracle at all β€” the adversary traded on the venues the oracle read and let the oracle faithfully report the result. In each case the mechanism ran exactly as specified. The specification was the vulnerability.

More oracles do not produce more truth

The reflexive institutional answer to oracle risk is redundancy. Run three feeds, take a median, add a fallback. It sounds like defense in depth. It very often is not.

Redundancy reduces variance when the inputs are independent and increases confidence in error when they are not. Most crypto price feeds are not independent. They draw from the same venue APIs, weight by the same volume estimates, share the same outage windows, and inherit the same stablecoin assumptions. A median across three feeds that all read the same three venues is a median of one observation reported three times. It produces a tighter-looking number and identical tail risk, which is the worst of both worlds: the risk becomes harder to see without becoming smaller.

The same critique applies to bridge verification designs. A cross-chain layer that relies on an oracle to attest to source-chain state and a separate relayer to deliver the message can be described as having two independent parties. The independence assumption is doing all the work. If the oracle and the relayer share operators, infrastructure, or an incentive structure β€” which they frequently do, because the economics of running one role make running both attractive β€” then the effective threshold is one, and the design security claim describes two processes rather than two parties. The architecture makes this explicit: security is defined by who operates each role, and default configurations have historically been narrower than the marketing language suggests. A verification mechanism whose threshold is controlled by configuration is a governance surface, not a cryptographic guarantee.

This is the same lesson as the floored feed arriving through a different door. The protocol publishes an architecture diagram with N parties and operates with one. The gap is documented in a configuration file nobody audits, because configuration is not code and configuration is not reviewed.

The fragmentation that is not a scale problem

There is a related pattern I want to name, because it affects how this risk gets distributed.

Over the past three years the industry has funded an enormous expansion of execution environments. Dozens of rollups and app-chains now exist, each with its own sequencer, its own bridge, its own oracle deployment, and its own collateral markets. The stated rationale is scaling. The observable outcome is that a roughly stable population of active capital now sits distributed across a multiplying set of venues, each with shallower books than the single venue it replaced, and each running its own independent price feed and its own independent set of configuration parameters.

That matters for the null-return problem in a direct and underappreciated way. Oracle floors and heartbeats and cardinality minimums are calibrated against expected depth. When depth fragments across ten venues, each venue's feed is calibrated against a fraction of the liquidity that used to underwrite a single feed. The parameters that were safe on a deep market are not safe on a shallow one, and the parameters are copied from venue to venue as a template. A new rollup launching an oracle integration today will inherit the heartbeat interval of a market ten times its size, because that is what the documentation says.

The result is that the same configuration drift now exists in more places simultaneously, sharing a common failure trigger. When a broad market event occurs, every one of those shallow feeds has a floor problem at once. This is not diversification of risk. It is replication of risk with the appearance of distribution.

Custody, MPC, and the key-share N/A

In 2024, as spot Bitcoin ETFs came online, I spent several months reviewing the custodial wallet architectures that institutional asset managers were deploying. The public conversation was about custody as a legal and operational construct. The cryptographic conversation was about threshold signature schemes and multi-party computation, and that is where the null-return problem reappeared in unfamiliar clothing.

A threshold signature scheme distributes key material across n parties such that any t of them can produce a signature and any t minus one learns nothing. The security claim is a function of the threshold and the independence of the shares. What I found, in more than one deployment, was a gap in the share distribution protocol rather than in the signing protocol. The signing ceremony was well-specified and audited. The question of what happens when a designated shareholder is unavailable β€” the key-share equivalent of a null return β€” was handled by an operational runbook, not by the protocol.

I identified three attack vectors in the threshold aggregation path during that review and reported them privately. Two were accepted and patched. The third was declined as out of scope, because it required an assumption about participant collusion that the vendor argued was inconsistent with the contractual arrangement. That argument is coherent as a legal matter and irrelevant as a cryptographic one. The protocol does not read the contract. It reads the shares.

What struck me most was not the finding. It was that the marketing documentation described the scheme as eliminating single points of failure, while the actual threshold configuration, in at least one production deployment, allowed a quorum that a single operational compromise could assemble. The public claim was architecture. The private reality was configuration. The gap between them was invisible to every reader who did not ask for the deployment parameters, and nobody asks for deployment parameters.

The absence of a price is a governance decision

I want to push the framing one step further, because I think the technical community has been too generous with itself.

When a protocol designer chooses a floor for a feed, or a heartbeat interval, or a minimum observation cardinality, or a fallback price, they are not choosing a technical parameter. They are answering a political question: in the moment this protocol cannot know the truth, what should it pretend to know? That answer determines who gets liquidated, who gets to borrow, who absorbs the loss, and which counterparties are exposed. It is a distributional choice among token holders, borrowers, and lenders, and it is made by engineers in a pull request.

The industry has built an elaborate culture of governance around parameters that matter far less β€” emission rates, fee splits, quorum thresholds, treasury allocations β€” and essentially none around the parameters that decide who is solvent when the market gaps. A lending market's liquidation threshold gets weeks of forum debate and multiple votes. Its oracle heartbeat is set in a config file and never mentioned again. The two decisions have comparable impact on the same population, and only one of them is treated as a decision.

I do not think this is fixable by better tooling alone. Auditors can be taught to check answeredInRound. They can be taught to check observationCardinality. What is harder to teach is the habit of asking, for every external read, what this value means when the source cannot know. That is not a checklist item. It is a posture, and postures are not sold by the hour.

Takeaway: forecast the silent fallback

Here is where I think the next significant loss comes from, and it is not the direction most teams are watching.

The industry has spent this cycle hardening against the stale return. Staleness checks proliferated after 2022 and are now standard in anything with meaningful value. That is genuine progress, and it has shifted the attack surface rather than closed it.

The unguarded mode is the fallback. As protocols accumulate multiple feeds, TWAPs, and cross-chain attestations, they acquire decision logic that runs when the primary fails: use the secondary, use the last known good, use the governance-set conservative price. Every one of those branches is a null handler written by a human under time pressure during an incident, or more likely written months earlier during a design sprint and never revisited. In a market where the primary feed is failing, the conditions that broke the primary are usually present in the secondary too, and the fallback converts a detectable failure into an undetectable one. The protocol does not stop lending. It lends at the last number anyone trusted.

The exploit will not look like an exploit. It will be a borrower who notices that a protocol's collateral valuation has decoupled from the market and simply uses the protocol as designed. No flash loan. No reentrancy. No governance attack. A correctly executed transaction against a contract that answered the wrong question confidently, and passed every check it wrote for itself.

The defenses that matter are unglamorous and cheap. Expose every floor, ceiling, fallback, and heartbeat as a public view function so consumers can check them programmatically. Treat observationCardinality as a first-class integration parameter rather than a deployment detail. Write null handlers as reviewed code rather than inline defaults. Publish the governance decision about what the protocol pretends to know when it cannot know. And when you review a feed integration, ask the only question that has ever mattered.

If this source is wrong, how would the contract find out?

Most of them would not. They would get a number, well-formed, freshly stamped, signed by an oracle that was doing exactly what it was configured to do. Math does not negotiate, and it does not warn you either.