Default Settings



Zero-knowledge proofs sold one promise above everything else, you don't have to trust the team. Just check the math.

A decade of protocols, rollups, privacy systems, and identity infrastructure was built on that guarantee, the idea that cryptography could replace faith entirely.

Two protocols. One week. One skipped command.

Not a sophisticated attack. Not a novel vulnerability. A default value that shipped as a placeholder, sat untouched behind millions of dollars in user funds, and waited.

Veil Cash fell first - 2.9 ETH, gone in a single transaction.

A few days later, FoomCash lost $2.26 million to the identical flaw.

Both marked something the ZK ecosystem had never officially recorded before: A live exploit of deployed ZK cryptography, confirmed, in production.

The math wasn't broken. It was never finished.

If the entire premise of trustless systems is that the proof speaks for itself, what happens when the proof was hollow from the moment it was deployed?

Credit: CoinsBench, ZK Security, Tornado Cash, CoinDesk, Trail of Bits, 0xPARC, Veil.Cash, DK27ss, Pashov Audit Group, Defimon Alerts, Cryptopolitan, FoomCash, OtterSec, Expander, Solana, Dedaub

This vulnerability class is not new. Neither is the assumption that nobody would ever bother exploiting it.

In 2019, Tornado Cash's own team discovered a live bug in their circomlib circuit, a single character, = instead of <==, that would have let anyone fake a Merkle root and drain the contract.

They exploited it themselves before anyone else could, published a post-mortem, and moved on.

In 2021, Zcash quietly patched an infinite-mint flaw buried in their trusted setup parameters.

A vulnerability that had existed for years. Believed unexploited, because exploitation would have required, in their words, "a high level of technical and cryptographic sophistication that very few people possess."

The lesson that was filed away: The cryptography is sound, but the code around it is hard enough that malicious actors won't find the gaps first.

Complexity as a moat. Sophistication as a deterrent.

By 2022, Trail of Bits had named a whole family of these failures, "Frozen Heart," Fiat-Shamir implementation bugs where public inputs weren't bound to the hash transcript before challenge generation.

SnarkJS, Dusk Network, ConsenSys' gnark - all affected. The paper was published. The bug class was documented. 0xPARC built a public tracker cataloguing the patterns.

Researchers catalogued. Teams patched. The ecosystem nodded and moved on.

The unspoken consensus held: ZK code is too arcane to exploit at scale. Attackers won't read the papers. They won't do the math.

Nobody asked what happens when someone finally does.

What changes when the PoC is already written and posted on GitHub?

The First Shot

Around February 20th, a small privacy pool on Base called Veil Cash went to zero.

Not hacked in any dramatic sense. No flash loan. No oracle manipulation. No insider. Just a single transaction that looped 29 times, pulling 0.1 ETH on each pass, until the pool was empty.

Veil Cash was a Tornado Cash fork, permissioned deposits routed through a validator contract, zero-knowledge withdrawals, quiet and unassuming on a network full of noisier things.

The attacker used nullifier hashes that read like a taunt: 0xdead0000 through 0xdead001c.

Fabricated from nothing. Accepted without question.

The verifier didn't reject them because it couldn't.

Groth16 verification depends on gamma and delta being distinct, independent values generated during a trusted setup ceremony.

Skip that step, and both stay pinned to the BN254 G2 generator, the snarkjs Phase 2 default, a placeholder the toolchain expects users to replace before deployment.

Nobody ran the command.

Pashov Audit Group had audited Veil Cash. The verifier contract was out of scope for their engagement.

The attacker called withdraw() 29 times on the 0.1 ETH pool using fabricated nullifier hashes, 0xdead0000 through 0xdead001c, draining 2.9 ETH, without ever having deposited.

Total drain: 2.9 ETH, executed in a single transaction.

Drain Transaction: 0x5ff6dbc33e77fab8dc086bb9ea3c88f1ba81df198d24ec9fc0c5b50fb1a4a17d

DefimonAlerts, Decurity's security monitoring arm, intervened shortly after, rescuing 2.025 ETH from the remaining pools and returning it to Veil.Cash.

Whitehat Rescue Transaction:
0x42019ae0532a25e40068fa16d2c7e3a9ce006f0b872d84ae5b4540b368a0268c

Later, at ~22:05 UTC, the original exploiter returned the drained funds to Veil.Cash. Whether that was out of conscience, a whitehat testing the technique, or something else entirely, the chain doesn't say.

Exploiter Return Transaction: 0x5a16ada9fbb068cc2492df57694b25ea56734eb6599c8f269767a23d10e0d162

Veil.Cash confirmed that 100% of funds from the affected pools have now been recovered and secured.

But the technique was now documented and out in the public

DK27ss published a full proof-of-concept on GitHub.

CoinsBench followed with their own analysis, mechanics and all, laid out with plenty of updates.

The exploit wasn't a secret anymore. It was documentation.

2.9 ETH was the proof of concept. The question was who else had shipped the same placeholder, and how much was sitting behind it.

When the blueprint is public and the target is bigger, how long does it take for the next transaction to fire?

The Same Mistake, Six Times

FoomCash was one team, with one skipped command, with one verifier broken from day one.

We covered it in The Unfinished Proof, the bounty dispute, the EthSecurity Telegram standoff, the sanitized post-mortem that described an "elite white-hat response" while the blockchain recorded something else entirely.

The math was immutable. The narrative was not.

What it pointed to was harder to dismiss as a single team's failure.

Two days after FoomCash's March 1 post-exploit analysis, OtterSec published "Unfaithful Claims: Breaking 6 zkVMs."

Researchers Himanshu Sheoran and Valter Wik disclosed this Fiat-Shamir binding bug class across six independent implementations over several months from late 2025 to early 2026: Jolt (a16z), Nexus, Cairo-M (Kakarot Labs), Ceno (Scroll), Expander (Polyhedra), Binius64. Different teams. Different codebases. Different proof systems. Same failure.

While distinct from the FoomCash Groth16 verifier misconfiguration, it follows a similar soundness-failure pattern where implementation gaps let provers cheat verification.

If any value that affects a verification equation isn't hashed before the relevant challenge is derived, the prover can effectively see the challenge before choosing that value. Pick the value to make the equation pass. The proof verifies. The statement it proves can be completely false.

In each of the six systems, prover-controlled values - opening claims, lookup sums, public inputs - were feeding into verification equations without ever being absorbed into the transcript first.

The fix, in every case, was one or two lines of code.

Finding the bug required understanding the entire verification flow end to end and asking: What if the prover chose this value after seeing the challenge?

To demonstrate the severity, OtterSec published a live challenge: Submit a valid proof that you've found a counterexample to Fermat's Last Theorem (find a3+b3=c3a3+b3=c3 for integers a,b,c≥1a,b,c≥1).

In a deployed blockchain context, the same technique could translate to conjuring funds from nothing.

Most of the six were patched between October and January 2026, after private disclosure.

Ceno, Scroll's zkVM, had an open issue filed November 10. As of the OtterSec publication date, it remained open.

Expander's $500K bug bounty was still pending confirmation.

This wasn't a new bug class either. Trail of Bits documented "Frozen Heart" - the same Fiat-Shamir binding failure - in 2022, affecting Iden3's SnarkJS, Dusk Network's plonk, and ConsenSys' gnark.

Solana patched one instance quietly in April 2025 and a second in June 2025, both before any exploit landed.

The papers existed. The trackers existed. Six teams built systems independently and arrived at the same mistake anyway.

OtterSec's diagnosis of why it keeps happening is worth sitting with.

Academic papers describe interactive protocols, a real verifier sends random challenges in real time, so binding is implicit. They don't specify what to put in the transcript for the non-interactive version.

That responsibility falls to the implementor. In modular zkVM architectures, each layer assumes the adjacent layer handles it. Every hash operation has a cost, optimization pressure quietly pushes values out of the transcript on the logic of "probably fine."

Unit tests run honest provers. Integration tests run the honest prover. Nothing automated catches an adversarial one. Finding this requires a human who understands the full verification flow asking the one question most teams never asked.

Six zkVMs. Most carrying no live value at the time of disclosure, no funds were lost, no exploit landed. The bugs were found and reported before anyone with bad intentions got there. That matters.

But zkVM rollups are where the industry could be heading, the proof system underneath the chain, not just the contract on top of it.

How that scales when real value moves underneath it is, for now, still a hypothetical. The window for it to stay that way is narrowing.

Two different toolchains. Two different failure modes.

The skipped CLI step and the missing transcript binding are not the same bug.

But they are the same question, unanswered in the same way, who checked what the verifier was actually accepting?

Out of Scope

The question of who checked what the verifier was accepting has a structural answer. Nobody.

Because in most engagements, nobody was asked to.

Circuits get audited. Deployment doesn't. Verification keys, the cryptographic surface that determines whether a proof is accepted or rejected, are rarely shared with auditors.

It's not an oversight in the traditional sense. It's a boundary both sides agreed to, usually without considering what sits on the other side of it.

Pashov Audit Group audited Veil Cash. The misconfigured verifier was explicitly out of scope for their engagement. They noted it after the fact.

Not a criticism, scope is negotiated, and deployment parameters are often not shared with auditors at all.

But the result is a protocol that passed the audit and shipped a verifier that accepted any proof ever submitted to it.

zkSecurity, who published the Groth16 post-mortem after FoomCash was drained, were direct about their own exposure: The first thought when reviewing the vulnerability was that it could have happened to a project they had audited. Deployment is rarely in scope.. That was something they said they intended to change.

After the FoomCash exploit, Dedaub ran an EVM-wide scan for contracts to identify contracts that store the same element twice, using their advanced tooling. No other live contracts with significant locked value turned up. The known blast radius, on EVM chains at least, appeared contained.

The GitHub search found a different problem. A few repositories still carried the same δ=γ fingerprint.

One example: A Tornado Cash implementation built for educational purposes with 248 stars, the kind of repo developers clone as a starting point, deploy without reading the setup documentation, and assume is production-ready because someone else starred it.

The PoC for the Groth16 exploit is public.

So is the one that scaled it.

The repos carrying the vulnerable defaults are public. The gap between those two facts is measured in whoever hasn't found them yet.

zkSecurity is adding Phase 2 detection to zkao, their continuous monitoring scanner.

Five of the six affected zkVMs were patched between October and December 2025, each within weeks of private disclosure by Sheoran and Wik. Ceno remains open.

The ecosystem is reacting.

Reacting to what's already found is not the same as knowing what hasn't been.

How many verifiers right now are holding funds behind a setup ceremony someone meant to finish?

The promise was never the code. It was the math underneath it.

ZK proof systems were sold as the end of trust, you don't have to believe the team built it right, you just verify the proof.

That guarantee is only as strong as the setup that generated the parameters, the transcript that binds the prover's values, and the scope that someone, somewhere, agreed to check.

In the span of one week, two were exploited in public, against live funds, for the first time in the technology's history.

Not because the cryptography broke. Because the work that makes cryptography meaningful was either never done or never assigned to anyone.

Veil Cash was the proof of concept.

FoomCash was the proof it scales.

Six zkVMs were the proof that it isn't one team's failure.

PoCs are published.

The GitHub repos are still there.

Ceno's issue is still open.

Every disclosure is also a blueprint, and blueprints in this space have a shelf life measured in days.

The ZK ecosystem spent a decade telling users the math was the guarantee. It turns out the math needed a ceremony. The ceremony needed a command. The command needed someone whose job it was to run it.

Nobody had checked whose job that was.

When the only thing standing between a user's funds and an empty contract is a CLI step in a setup guide, who's responsible for making sure it ran?


기사 공유하기

REKT는 익명 작성자들에 의한 공공 플랫폼이며, REKT에 작성된 관점이나 내용에 대해서 그 어떤 책임도 지지 않습니다.

기부 (ETH / ERC20): 0x3C5c2F4bCeC51a36494682f91Dbc6cA7c63B514C

disclaimer:

REKT는 당사 웹 사이트의 익명의 작성자 또는 REKT에 의해 게시되거나 관련된 서비스에서 게시되는 콘텐츠에 대해 어떠한 책임도 지지 않습니다. 당사는 익명 작성자들의 행동 및 게시물에 대한 규칙을 제공하지만, 익명의 작성자가 웹 사이트 또는 서비스에 게시, 전송 혹은 공유한 내용을 통제하거나 책임지지 않으며, 귀하가 웹 사이트 또는 서비스에서 직면할 수 있는 불쾌함, 부적절함, 음란함, 불법 또는 기타 해로운 콘텐츠에 대해서도 책임을 지지 않습니다. REKT는 당사 웹 사이트 또는 서비스 사용자의 온라인 또는 오프라인 행위에 대한 책임을 지지 않습니다.