CheapbookZ

Market Prices

Coin Price 24h
BTC Bitcoin
$81,536 -0.93%
ETH Ethereum
$2,430.34 -1.73%
SOL Solana
$104.36 -1.78%
BNB BNB Chain
$720.2 -1.33%
XRP XRP Ledger
$1.3 -2.98%
DOGE Dogecoin
$0.0800 -2.65%
ADA Cardano
$0.2225 -3.51%
AVAX Avalanche
$9.95 -3.54%
DOT Polkadot
$1.14 -3.83%
LINK Chainlink
$12.11 -4.29%

Fear & Greed

55

Greed

Market Sentiment

Event Calendar

{{ๅนดไปฝ}}
08
04
upgrade Solana Firedancer

Independent validator client goes live on mainnet

28
03
unlock Arbitrum Token Unlock

92 million ARB released

18
03
unlock Sui Token Unlock

Team and early investor shares released

30
04
upgrade Celestia Mainnet Upgrade

Improves data availability sampling efficiency

15
04
halving Bitcoin Halving

Block reward reduced to 3.125 BTC

12
05
halving BCH Halving

Block reward halving event

22
03
unlock Optimism Unlock

Circulating supply increases by about 2%

10
05
upgrade Ethereum Pectra Upgrade

Raises validator limit and account abstraction

Altseason Index

41

Bitcoin Season

BTC Dominance Altseason

Gas Tracker

Ethereum 28 Gwei
BNB Chain 3 Gwei
Polygon 42 Gwei
Arbitrum 0.5 Gwei
Optimism 0.3 Gwei

Market Cap

All โ†’
1
Bitcoin
BTC
$81,536
1
Ethereum
ETH
$2,430.34
1
Solana
SOL
$104.36
1
BNB Chain
BNB
$720.2
1
XRP Ledger
XRP
$1.3
1
Dogecoin
DOGE
$0.0800
1
Cardano
ADA
$0.2225
1
Avalanche
AVAX
$9.95
1
Polkadot
DOT
$1.14
1
Chainlink
LINK
$12.11

๐Ÿ‹ Whale Tracker

๐ŸŸข
0xb626...6ffd
2m ago
In
28,240 BNB
๐Ÿ”ด
0x64cc...a5ec
12h ago
Out
1,392,816 USDC
๐Ÿ”ด
0x8695...daae
6h ago
Out
10,011,433 DOGE

๐Ÿ’ก Smart Money

0x72ee...70ff
Arbitrage Bot
+$3.1M
66%
0x31d6...ad4d
Early Investor
-$5.0M
85%
0xc6af...971a
Early Investor
+$0.7M
78%

๐Ÿงฎ Tools

All โ†’
Altcoins

The Silent Zero: When Empty Data Passes Every Check in On-Chain Systems

CryptoSam

The Silent Zero: When Empty Data Passes Every Check in On-Chain Systems

Hook

At 03:47 UTC, a liquidation keeper I was asked to review returned 0. The call was latestAnswer() against a stale aggregator. The function did not revert. The protocol did not pause. The position did not get liquidated. Twelve hours later the borrower withdrew collateral that should have been seized, and there was no exploit transaction to point at โ€” no flash loan, no reentrancy, no calldata trick. The system had been handed an empty value and had quietly decided that emptiness was a number.

Zero is a number. It is also the absence of one. The EVM does not distinguish between the two, and neither did this protocol. That is the entire attack. Not a broken signature check. Not an oracle bent to a wrong price. Just a feed that said nothing, and a contract that heard 0.

I have spent the better part of a decade pulling smart contracts apart, and the failures that keep me up at night are not the clever ones. They are the quiet ones. The ones where nothing happens, and nothing happening is exactly what the attacker wanted. Empty data is more dangerous than wrong data, because wrong data trips alarms and empty data passes every check.

Context

The oracle problem is usually framed as a question of truth: how does a contract know the real-world price of ETH? The framing hides a second, uglier question. How does a contract know it received an answer at all? These are not the same problem, and the second one is where the money leaks.

A price feed is a pipeline. An off-chain aggregator collects reports from independent node operators, computes a median, and writes the result into an on-chain contract. A consumer contract reads that storage slot through a view function. Every stage of that pipeline can fail โ€” and the way it fails matters more than the way it succeeds. When a node goes offline, when a heartbeat lapses, when a round has not been finalized, the aggregator does not throw. It returns the last thing it knows, or it returns the default. Defaults in the EVM are not errors. They are values.

This is not a bug unique to oracles. It is a property of the machine underneath. The EVM is a state machine with no concept of "missing." Every storage slot exists. Every mapping key resolves. The compiler guarantees that reads are total functions โ€” they always return something, even when there is nothing to return. Solidity inherited this from C and from the systems languages before it: the default value of a type is baked in, and reading an unset variable is not a fault.

So the chain is full of zeros that mean nothing, and contracts are full of checks that cannot tell a meaningful zero from an empty one. The ledger remembers what the wallet forgets, but it also remembers plenty of things that were never true. It records the default. It records the lazy initialization. It records the round that never completed. And a downstream reader that trusts structure over semantics will treat all of it as fact.

The industry has spent enormous energy defending against wrong data. Slippage checks, TWAPs, deviation bounds, circuit breakers โ€” the whole apparatus assumes the attacker is trying to move a number. Almost none of it assumes the attacker is trying to remove one. That gap is the subject of this piece. I am going to walk through how empty data enters on-chain systems, why the standard defensive patterns miss it, and where I think the next wave of losses is hiding.

Core

1. The Default Value Is Not a Neutral Value

Open any Solidity contract and write this:

mapping(address => uint256) public balances;
function get(address who) external view returns (uint256) {
    return balances[who];
}

Call get for an address that has never interacted with the contract. You get 0. No revert. No event. No distinction between "this user has a balance of zero" and "this user does not exist." The type system cannot express the difference, so the contract cannot either.

Now replace balances with prices, or feedRound, or collateralRatios, or votesFor. The same silence applies. A missing price is 0. A missing round is 0. A missing vote is 0. A missing collateral ratio is 0 โ€” which, in a liquidation formula, reads as infinitely undercollateralized. A missing price in a swap formula reads as a free asset.

The default value is a confession the code does not know it is making. It says: I have no information, and my answer is zero. For a counter, that is fine. For a price, an amount, a timestamp, or a governance tally, it is a loaded gun pointed at whoever reads the value next.

I watched this pattern surface in the 2021 NFT mint craze, in a different guise. A generative art clone I audited had no access control on its mint function. That is a presence-of-data bug โ€” anyone could call it. But the twin bug is the absence-of-data bug: an owner field that defaults to address(0), a require(owner != address(0)) that nobody wrote, a treasury whose recipient resolves to the burn address. Same class. Same silence. Code is law, but bugs are the human exception โ€” and the exception is usually that a human forgot to ask whether the value existed.

2. Reading an Oracle the Wrong Way

Here is the single most common oracle bug in production DeFi, and it has been common for years:

(, int256 answer, , , ) = aggregator.latestRoundData();
return uint256(answer);

The tuple from latestRoundData is (roundId, answer, startedAt, updatedAt, answeredInRound). Four of those five fields are checks in disguise. The typical vulnerable consumer throws all of them away and reads answer. That is the equivalent of accepting a check and only looking at the amount, never the signature, never the date, never the payee.

When an aggregator has not produced a fresh round, updatedAt goes stale. When a round is in progress, answeredInRound can lag behind roundId. When a feed is deprecated or misconfigured, answer can be 0. When a feed is fundamentally broken, answer can be a negative value cast into a uint256, wrapping into something near 2^256.

The canonical defense looks like this:

(uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) =
    aggregator.latestRoundData();

require(answer > 0, "non-positive answer"); require(updatedAt != 0, "round not complete"); require(answeredInRound >= roundId, "stale round"); require(block.timestamp - updatedAt <= MAX_STALENESS, "stale price"); return uint256(answer); ```

Four claims. Every one of them is an existence claim, not a correctness claim. We are not asking whether the price is right. We are asking whether a price is. That distinction โ€” existence versus correctness โ€” is the fault line running under most of DeFi security, and most protocols only defend the second side of it.

The reason this matters right now, in the middle of a bull market, is that new feeds are spawning faster than anyone can audit them. A freshly funded protocol with a nine-figure TVL lists a long tail of assets โ€” liquid staking derivatives, restaking receipts, exotic LP tokens, wrapped vault shares โ€” and each one needs a feed. Feed proliferation is where the empty-data risk grows fastest, because a thin feed on a brand-new asset is exactly the kind of feed that goes stale, that skips rounds, that returns zeros during the quiet hours. The most dangerous value a contract can return is not a manipulated price. It is an unfinished sentence.

3. The Validated Feed Wrapper and Where It Still Fails

Because raw reads are so error-prone, teams wrap them. A PriceOracle contract sits between the aggregator and the protocol, exposes a single getPrice() function, and centralizes the checks. This is good engineering. It is also where the failure migrates upward.

Consider a wrapper that catches feed errors and falls back to a secondary source:

function getPrice(address asset) external view returns (uint256) {
    try primary[asset].latestRoundData() returns (
        uint80 id, int256 answer, , uint256 updatedAt, uint80 answeredInRound
    ) {
        if (_isValid(id, answer, updatedAt, answeredInRound)) {
            return uint256(answer);
        }
    } catch {}

// fallback (uint256 fbPrice, uint256 fbTs) = fallback[asset].peek(); require(block.timestamp - fbTs <= MAX_STALENESS, "fallback stale"); return fbPrice; } ```

Two things can go wrong, and both are empty-data problems. First, the catch {} block is empty. A revert from the primary becomes a silent handoff to the fallback. If the fallback is also empty or stale, the require catches it โ€” good. But if the fallback returns a default rather than reverting, the emptiness propagates. Second, the wrapper itself can be called during a window where the primary has an in-progress round and the fallback has not updated. The wrapper does not return a wrong number; it returns an unreliable one, and the caller cannot tell.

The subtle part is that a wrapper like this looks rigorous. It has a try/catch. It has a require. It has a fallback. It satisfies every auditor checklist except the one that matters: what does this function return when the system has nothing to say? If the answer is "some number," the wrapper has not solved the empty-data problem. It has renamed it.

I ran into exactly this shape during a 2020 audit of stablecoin swap mechanics in the Curve lineage. The invariant math was elegant; the whitepaper derived it cleanly. But the precision handling around the amplification coefficient meant that at the edges โ€” during high volatility, when the system was leaning hard on its reserves โ€” the numbers drifted in ways the derivation did not cover. Mathematical elegance does not guarantee security. It only guarantees that the failure will be elegant too. The same is true of a well-typed oracle wrapper: it will fail in a way that passes review.

4. `try/catch` and the Swallowed Revert

Solidity's try/catch is a gift and a hazard. It lets a contract handle an external call failure gracefully. It also lets a contract hide one.

function _refresh() internal {
    try oracle.update() {
        lastUpdate = block.timestamp;
    } catch {
        // keep going with the old data
    }
}

This is defensible when the old data is known-good and time-bounded. It is catastrophic when the old data is a default. The catch block does not record the failure. It does not emit an event. It does not set a degraded flag. From the outside, the system looks identical whether update() succeeded or reverted every single time. A swallowed revert is an empty log entry โ€” and an empty log is indistinguishable from a healthy one.

This is the same pathology as the pipeline that returned nothing. A system that returns an error is diagnosable. A system that returns cleanly while doing nothing is not. Wrong data triggers a red alert; empty data, handled by an empty catch, triggers nothing at all.

When I dissected the 2022 lending collapse that wiped out a set of positions, the headline was reentrancy โ€” a missing mutex, a state update after an external call. But underneath the reentrancy there was a subtler lesson about time. The liquidation path relied on a price refresh that could, in certain call orderings, complete without updating anything, because the update was inside a try/catch that had been added to "improve robustness." The mutex was the obvious bug. The swallowed update was the quiet one. Only one of them was in the post-mortem thread.

5. Multicall and the Illusion of Atomicity

Multicall is one of the most trusted primitives in the ecosystem. Bundle many reads into one, get many results back. It is also a poster child for partial-failure ambiguity.

A classic multicall loop over calls:

function aggregate(Call[] calldata calls)
    external view returns (uint256[] memory results)
{
    results = new uint256[](calls.length);
    for (uint256 i = 0; i < calls.length; i++) {
        (bool ok, bytes memory data) =
            calls[i].target.staticcall(calls[i].data);
        if (!ok) { results[i] = 0; continue; }
        results[i] = abi.decode(data, (uint256));
    }
}

If call #3 fails, results[3] is 0. The caller receives an array that looks successful. There is no partial-failure signal, no index of failure, no revert. A consumer that sums the array gets a number lower than the truth and no way to know. A consumer that reads index 3 gets a zero and no way to know whether zero is the answer or the absence of one.

In batch-heavy systems โ€” vault accounting, portfolio valuation, lending health checks across many assets โ€” this is a live footgun. The fix is to carry an ok flag alongside each result, or to revert the whole batch on any failure, or to reserve a sentinel value that cannot collide with a legitimate answer. Each has trade-offs, and almost none of them get chosen, because the happy path works in testing and testing rarely exercises the empty path.

6. Governance: An Empty Vote Is Not a No Vote

Move from prices to votes, and the same silence shows up with worse consequences.

function quorumReached(uint256 proposalId) public view returns (bool) {
    return proposals[proposalId].votesFor >= quorum();
}

proposals[proposalId].votesFor defaults to 0 for a proposal that does not exist. If quorum() ever evaluates to 0 โ€” a misconfigured parameter, a division that truncates, a token with zero total supply at deployment, a snapshot taken before the token was minted โ€” then a proposal that was never created, never voted on, and never reviewed can satisfy quorumReached. Execution of an empty proposal then runs whatever the calldata says.

Even when quorum is nonzero, the empty-data problem is subtler. A governance system that counts "abstain" as neither for nor against, and that treats an unset vote as abstain, effectively lets silence influence outcomes. If quorum counts total votes cast, abstentions inflate participation while suppressing consent. A proposal can pass with a minority of actual support and a majority of indifference. That is not a bug in the formal sense. It is a design bug, and design bugs around empty inputs are the hardest to fix because the failure is a governance outcome rather than an exception.

The Silent Zero: When Empty Data Passes Every Check in On-Chain Systems

I keep coming back to a simple line for times like this. The ledger remembers what the wallet forgets โ€” but it also fails to record what nobody did. Absence is data. The problem is that Solidity's type system cannot be bothered to say so.

7. AI Agents and the Stale-Feed Race

Now the part that is genuinely new, and genuinely under-defended. Autonomous agents are executing on-chain strategies at machine speed. In 2026 I audited a protocol designed for exactly this: AI-driven DeFi strategies reading oracle inputs, deciding positions, and submitting transactions within milliseconds.

The vulnerability I isolated was not a manipulation of the feed. It was a race on the feed's freshness. An agent's strategy module polled latestRoundData() and evaluated a state. Between the poll and the execution transaction, the round changed โ€” a new answer arrived, or the previous round was superseded, or an updatedAt aged past a threshold. The agent's decision was computed on one state and executed against another. The protocol's validation covered the price's magnitude but not the price's identity. So the agent acted on what was, by execution time, effectively empty context.

I built a formal verification model for the temporal inconsistency โ€” a small state machine asserting that any consumed price must share a round identifier with the round that was evaluated. The core team adopted it. But the lesson generalizes far beyond one protocol: agents that read and then act across a time boundary need a proof that the data they read is the data they act on. Humans absorb this fragility with latency. Machines do not. The faster the execution loop, the narrower the window in which a value can be empty, superseded, or default โ€” and the more valuable it becomes to whoever can widen it.

This is where I think the next category of exploit lives. Not the oracle manipulated to a lie, but the oracle caught mid-sentence, with a bot that treated an unfinished thought as a market signal.

8. Empty Batches in the Proving Layer

The empty-data pathology is not confined to the application layer. It reaches into the infrastructure that is supposed to make the system trustworthy.

ZK rollups prove batches of transactions. Proving is expensive โ€” the prover cost per batch is dominated by fixed overhead plus per-transaction cost. An empty batch still requires a proof if the system insists on a proof for every batch, and the cost of proving an empty batch is close to the fixed overhead of the prover. So a liveness failure at the sequencer level โ€” no transactions to batch โ€” still bills the operator. Worse, some designs allow an empty batch to be posted as a valid update. If an empty state transition is accepted without the full check set, a gap in coverage opens: the chain advanced, the state root changed (or did not), and downstream consumers cannot tell whether a batch contained real activity or none at all.

The economics here are brutal and, in a bull market, easy to ignore. Proving costs are absurdly high relative to the fees most rollups actually collect; unless gas returns to bull-market peaks, operators bleed. Adding the empty-batch case to the cost model โ€” proving nothing, expensively, to maintain liveness โ€” is a line item that rarely appears in token models. A chain that treats an empty batch as a valid batch has inherited the EVM's default-value problem at the protocol layer.

9. Stablecoin Attestations and the Empty Reserve Slot

Regulatory clarity, another place to look for silence. Europe's MiCA framework promises certainty for stablecoins and CASPs. Certainty, in practice, is a reporting pipeline โ€” and reporting pipelines are pipelines.

Consider an attestation feed: a reserve report, a custodian statement, a supply reconciliation. These arrive as data, and they feed dashboards, on-chain oracles, and risk engines. The regulatory requirement is that reserves exist and are adequate. The operational requirement, which nobody writes into the rule, is that a missing attestation must not read as an adequate one.

A stablecoin whose reserve data updates weekly can go a day, or a week, with a stale slot. If a consumer reads that slot without a freshness check, the stablecoin appears fully backed while nothing has been verified. And a small issuer facing MiCA's reserve and CASP compliance costs has a strong incentive to publish less, not more โ€” because every disclosure is a cost, and an empty slot is cheaper than a filled one. The regulation assumes data will be present. The system, left alone, optimizes toward its absence. Compliance frameworks define what must be true; they rarely define what happens when the proof of truth goes missing.

10. Hooks, Custom Logic, and Data Ownership

Finally, the layer where all of this becomes someone's responsibility. Programmable pools and hook-based architectures โ€” Uniswap V4 being the obvious example โ€” let developers inject arbitrary logic into the swap path. That is powerful. It is also a transfer of custody over data integrity. A hook can read an oracle, apply a fee, gate a transfer, redirect a flow. Every one of those reads is a potential empty-data read. Every custom hook is a new place where a 0 can mean a price, or nothing.

The complexity spike here is real. The developer population that can safely write and reason about hook-level invariants is far smaller than the population that will deploy hooks anyway, because the market rewards shipping. When I look at where the empty-data problem will get worse before it gets better, I do not look at the flagships. I look at the long tail of custom logic sitting on top of shared liquidity, each one carrying its own assumptions about what a value means when it is absent. Programmable layers multiply the number of places a default can be mistaken for a fact.

Contrarian

Here is where I part ways with most of the security narrative. The industry's mental model is that attackers manipulate data toward a value they prefer. So defenses focus on bounding the value: max deviation, time-weighted averages, price bands, circuit breakers. This is a solver's instinct โ€” find the wrong number, constrain it.

But the more efficient attack has always been to remove the number. There is no deviation to bound on a missing price. There is no time-weighted average of nothing. A circuit breaker that triggers on "price moved more than X%" does not trigger when the price vanishes, because vanishing is not moving. The entire defensive apparatus is a system of comparisons against a baseline, and empty data attacks the baseline itself.

The blind spot is structural, not accidental. Auditors are trained to ask "can this value be wrong?" We are much less trained to ask "can this value be absent, and if so, what does the code do?" The first question has a rich toolchain. The second has a checklist entry that people skip because the happy path never exercises it. I have seen audits spend days on rounding and signedness while never once testing the path where a feed returns 0. Wrong-priced data is a loud failure. Empty data is a silent one, and silent failures are the most expensive kind because they compound before anyone notices.

The uncomfortable implication: the most underpriced risk in on-chain systems right now is not the malicious input. It is the missing one.

Takeaway

Watch for the checks that are not there. When the next protocol ships in this bull market, do not ask whether its oracle can be manipulated. Ask what it returns when the oracle says nothing โ€” and whether anyone wrote a line of code that could tell the difference. The zeros are already on-chain. The only question is which contract reads one next.

Code is law, but bugs are the human exception. The empty value is the exception we have not learned to see.