The Empty Calldata Problem: Why Systems That Succeed on No Input Are Already Exploited

CryptoWolf • • Altcoins

Last week, a pipeline I was stress-testing did something production systems almost never do. It stopped. An upstream job handed it an empty payload — nine fields, all blank, no title, no source, no datapoints — and rather than hallucinate a result to fill the shape of its own template, the process returned one verdict: insufficient input, analysis refused. No output. No fabrication. A clean halt at the boundary.

I sat with that for a while. Because across twelve years of auditing smart contracts, the most expensive failures I have documented share a single trait: the system kept running after it should have stopped. The pipeline was not clever. It was merely honest about the void in front of it. In Solidity, that honesty is almost never free — and almost never present.

There is a design axiom that separates resilient systems from fragile ones, and it has nothing to do with how much code they contain. Ask instead: what happens when the input is wrong? Two answers exist. Fail-open systems proceed with a default — assume the missing value is zero, assume the absent signer is trusted, assume the empty array means "all." Fail-closed systems halt. They treat absence itself as an error condition, not as a permission.

Ethereum's execution model is brutally explicit here. Call a function with malformed calldata and the EVM reverts before a single storage slot mutates. But that protection ends the instant the contract itself decides to interpret absence as intent. The compiler checks the shape of your input; it never checks your assumptions about what that shape means. This is where the expensive mistakes live, and it is the terrain I map for a living. The distinction is not academic. It is the difference between a protocol that pauses and a protocol that pays out.

For context: the protocol mechanics that matter are dull and repeated. A user calls a function. The function validates. The function mutates state. The function settles value. Most audits spend their budget on the third and fourth steps. Almost no one audits the second step when the input is empty.

Consider a liquidation routine I reviewed in 2019. Stripped to its essentials:

function liquidate(address user, address[] calldata collateral) external {
    for (uint i = 0; i < collateral.length; i++) {
        _seize(user, collateral[i]);
    }
    _settle(user);
}

The loop has no length check. Pass an empty array and the for-loop never executes — zero iterations, zero reverts — and control flows straight into _settle(user), which closes the debt against a position whose collateral was never seized. The function "succeeded." The invariant collateral_value >= debt_value * threshold was never evaluated, because the code never reached the branch that evaluates it. A function that succeeds on empty input is not a function; it is an unbounded promise.

This is not a reentrancy bug. No attacker re-enters. No gas war. No flash loan. The exploit is the absence of an argument — and absence does not appear in any call trace a monitoring tool would flag, because nothing anomalous executed. Nothing executed at all. In 2018, isolating a state-change ordering flaw in a lending protocol's liquidation logic, I learned the same lesson from a different angle: theoretical security models fail against runtime execution flaws, and runtime is where empty inputs live.

The same pattern recurs at the oracle layer. latestRoundData() returns a tuple; almost every integration I dissect omits the updatedAt staleness check:

(, int price, , , ) = feed.latestRoundData();
require(price > 0);

If the feed stalls — and feeds do stall, during volatility, during sequencer outages, during the exact windows when liquidations are most profitable — the contract reads a frozen price and treats it as current. The input is not empty in the calldata sense. It is empty in the semantic sense. Code does not lie, but it does hide — here it hides the difference between a value and a memory of a value.

Zero-address defaults belong to the same family. address(0) is the EVM's null, and a surprising number of constructors, setters, and ownership transfers accept it without complaint. Set an oracle address to zero, and latestRoundData() on a null contract returns nothing; the calling code, written to fail-open, reads that nothing as a default and proceeds. Root keys are merely trust in hexadecimal form — and an unset root key is trust granted to whoever moves first.

I stress-tested this class of failure in a local testnet environment years ago, manipulating invariant math under extreme liquidity imbalance. The lesson was not that the math broke. The lesson was that the math was never invoked, because the inputs that would have triggered it arrived empty and the system declined to notice. Velocity exposes what static analysis cannot see — but absence exposes what velocity cannot reach.

And there is a second-order version of the same disease. A multisig that requires "2 of 3" signers but treats an empty signature set as satisfying quorum by default — a fail-open on the governance layer. I mapped exactly this class of byte-level access-control discrepancy in the aftermath of a $611 million bridge hack, and the finding was never a human error. It was architecture. The system was built to proceed.

The meta-problem is structural, and it is the reason I keep writing about it. Static analyzers are trained to find bad code. They are not trained to find missing code. An empty array is syntactically perfect. A stale oracle is a valid uint256. A zero address is a valid address. Every one of these inputs passes type-checking, and type-checking is where most security tooling stops.

Here is the blind spot the industry refuses to price. We reward systems that always answer. A model that says "I don't know" gets rated worse than a model that confabulates, even when the confabulation is wrong. A protocol that halts on ambiguous input gets called "broken" by users, while a protocol that quietly proceeds gets called "smooth." Security is a process, not a product — and a process that cannot say "no" is not a process. It is a liability wearing a user interface.

The contrarian claim: the next wave of exploits will not be reentrancy, and they will not be oracle manipulation in the familiar sense. They will be null-input attacks — transactions that pass validation precisely because they pass nothing. No anomalous value, no suspicious calldata, no obvious victim. Just a function called with an argument that means nothing, executing a branch that assumes it meant something.

This is also why formal verification, for all its rigor, keeps missing these. You cannot prove a property about a state transition that never occurs. The invariant holds — vacuously — on the empty path. Infinite loops are the only honest voids; a skipped loop is a dishonest one, because it looks like it ran.

The Empty Calldata Problem: Why Systems That Succeed on No Input Are Already Exploited

The most dangerous function in your codebase is not the one with a reentrancy guard missing. It is the one that returns success when it should have reverted. Audit for absence first: empty arrays, zero addresses, stale timestamps, default booleans. Then ask the only question that matters. When your system receives nothing, does it stop — or does it invent?