A single line of missing bitwise masking. That was the difference between a valid proof and a forged one. In February 2025, a routine audit of zkSync Era’s batch verification circuit revealed a critical flaw: the verify_batch function did not constrain the number of public inputs within the allowed range. The fix was deployed silently. No public disclosure. No CVE. No post-mortem.
I spent six months building a minimal Groth16 implementation in Rust during the 2022 bear market. I know how fragile these circuits are. When I first saw the patch commit on GitHub—a single line added, removing the ability to overflow the public input array—I knew exactly what it meant: an attacker could craft a valid proof for an invalid state transition. The math doesn’t negotiate. If the circuit doesn’t enforce constraints, someone will exploit the gap.
Context: How Zero-Knowledge Rollups Verify Batches
zkSync Era, like other ZK-rollups, uses a recursive proving system. Each batch of transactions is compressed into a single validity proof. The sequencer submits the proof on-chain. The verifier contract checks that the proof corresponds to a valid execution. If the proof passes, the state is updated.
The key component is the batch verifier—a smart contract that performs the pairing check on the Groth16 proof. Inside that contract, there is a loop that processes public inputs. In zkSync’s implementation, the number of public inputs was derived from the proof itself, not from a separate trusted source. This is the locus of the bug.
Core: The Integer Overflow in Input Length
The vulnerability existed in the _verify_batch function inside ZkSyncVerifier.sol. The public inputs were stored in a dynamic array. The length of that array was read from the proof data. The circuit expected exactly 32 public inputs for a batch. However, the verifier contract did not explicitly check that publicInputs.length == 32. Instead, it used a loop that iterated up to publicInputs.length.
If an attacker submitted a proof with publicInputs.length set to a value greater than 32, the loop would read garbage from memory. But the critical risk was the opposite: setting publicInputs.length to 0. In Solidity, an empty array has a length of 0. The loop would not execute. The pairing check would proceed with default values. Under certain conditions, this allowed a forged proof to pass verification.
I traced the exact exploit path by reconstructing the verifier’s state machine. The attacker would craft a proof where the public inputs array is empty. The verifier would still compute the pairing equation, but with a nullified input term. The circuit’s degree of freedom—the ability to choose any public input—was no longer constrained. The result: the verifier accepted a proof of a state transition that never happened.
Trade-offs in the Design Choice
Why did the zkSync team miss this? Because they optimized for gas. Iterating over a dynamic array and checking its length costs 100−200 gas. Adding a require statement adds a fixed cost of about 500 gas. For a batch verifier that processes thousands of transactions per day, the gas savings compound. But privacy is a feature, not a bug—and so is security. The trade-off between gas efficiency and constraint enforcement is classic. Many L2 teams prioritize throughput over correctness.
Contrarian: The “Centralized Escrow” Blind Spot
The common narrative is that ZK-rollups are secure because they inherit Ethereum’s security. That is true at the protocol level. But at the implementation level, the verifier contract is a single point of failure. If the contract has a bug, the entire rollup’s state is compromised. No number of validators or decentralized sequencers can fix a broken verifier.
Moreover, the silent patch raises questions about responsible disclosure. The bug was discovered by a third-party auditor in a routine engagement. The patch was merged without a public advisory. In a bear market, developer resources are thin. Teams rush to release features instead of hardening code. This is the real threat to ZK-rollups: not the math, but the human decisions around code deployment.
Takeaway: Expect More Verifier Bugs in 2025-2026
As the number of ZK-rollups grows, so will verifier bugs. The trend toward recursive proofs and cross-chain verification compounds the complexity. Each new proof system—Groth16, PLONK, Halo2—has its own constraint pitfalls. I forecast at least three major verifier exploits before the end of 2026. Teams should publish their verifier code for peer review before mainnet upgrades. Silence before the audit is acceptable; silence after the fix is not.
Code is law, but bugs are reality. The missing line in zkSync’s verifier was a reminder that every line of on-chain code is a liability. Math doesn’t negotiate—but developers can. And they did, by removing the gas-inefficient check. The next bug might not be caught by a friendly auditor. It might be caught by an attacker. And by then, the only negotiation left will be in the form of a drain event.