Zero-knowledge proofs are supposed to be the gold standard of trustless verification — mathematical guarantees that something is true without revealing why. But what happens when the verifier itself is broken? Not the math. Not the circuit. The deployment configuration.
In February 2026, FOOMCASH — an Ethereum and Base ZK-proof lottery protocol — lost approximately $2.26 million because a single parameter in their Groth16 verifier was misconfigured: delta2 was set equal to gamma2. This one-line error destroyed the soundness of their entire proof system, allowing attackers to forge proofs and drain funds at will.
This article breaks down the cryptographic mechanics of this exploit, why it worked, and what every team deploying zkSNARK-based protocols needs to learn from it.
Background: FOOMCASH and the Privacy Promise
FOOMCASH operated as a decentralized lottery protocol using zkSNARKs (specifically Groth16) to provide privacy for participants. The architecture follows the well-known Tornado Cash model: users deposit tokens into a pool, and later withdraw them using a zero-knowledge proof that demonstrates knowledge of a valid deposit — without revealing which deposit is theirs.
The security model relies on two pillars:
- Nullifier tracking — Each withdrawal uses a unique nullifier hash to prevent double-spending
- zkSNARK verification — A Groth16 proof ensures the withdrawer actually made a deposit
The second pillar collapsed completely.
Groth16 Verification: A Primer
To understand the exploit, we need to understand how Groth16 verification works at a high level.
A Groth16 proof system consists of three phases:
- Setup: A trusted setup ceremony generates proving and verification keys
- Proving: The prover generates a proof π = (A, B, C) using private inputs (the "witness")
- Verification: The verifier checks the proof against public inputs using the verification key
The verification equation in Groth16 is a pairing check:
e(A, B) = e(α, β) · e(∑ aᵢ·Lᵢ, γ) · e(C, δ)
Where:
-
e()is a bilinear pairing function -
A,B,Care the proof elements -
α,β,γ,δare verification key parameters from the trusted setup -
Lᵢare precomputed values for each public inputaᵢ
The critical security property is soundness: it should be computationally infeasible to produce a valid proof (A, B, C) for false public inputs without knowing the correct witness.
This soundness depends on γ and δ being distinct, independently generated values. Here's why.
The Fatal Flaw: When delta2 == gamma2
In the verification equation, γ and δ serve different roles:
-
γgates the public input terms -
δgates the proof element C (which encodes the private witness)
When γ = δ, the equation becomes:
e(A, B) = e(α, β) · e(∑ aᵢ·Lᵢ, γ) · e(C, γ)
This can be simplified to:
e(A, B) = e(α, β) · e(∑ aᵢ·Lᵢ + C, γ)
This is devastating. Now the public input terms and the proof element C are in the same algebraic subspace. An attacker who has one valid proof can algebraically adjust C to compensate for any change in the public inputs.
In concrete terms: given a valid proof (A, B, C) for public inputs [x₁, x₂, ...], the attacker can compute a new C' that satisfies the verification equation for arbitrary public inputs [x₁', x₂', ...] — without knowing any witness at all.
The math:
C' = C + ∑ (aᵢ - aᵢ') · Lᵢ
That's it. Simple elliptic curve point addition. No brute forcing. No cryptographic breakthrough. Just exploiting a parameter that should never have been equal.
The Attack in Practice
The attacker exploited this against the FOOMCASH contracts on both Ethereum mainnet and Base. Here's how the withdrawal function worked:
function withdraw(
uint256[2] calldata _pA,
uint256[2][2] calldata _pB,
uint256[2] calldata _pC,
bytes32 _root,
bytes32 _nullifierHash,
address _recipient,
address _relayer,
uint256 _fee,
uint256 _refund
) external payable nonReentrant {
require(!nullifierHashes[_nullifierHash], "The note has been already spent");
require(isKnownRoot(_root), "Cannot find your merkle root");
require(
verifier.verifyProof(
_pA, _pB, _pC,
[uint256(_root), uint256(_nullifierHash),
uint256(uint160(_recipient)), uint256(uint160(_relayer)),
_fee, _refund]
),
"Invalid withdraw proof"
);
nullifierHashes[_nullifierHash] = true;
_processWithdraw(_recipient, _relayer, _fee, _refund);
}
The attack steps:
- Obtain one valid proof — Either from a legitimate deposit or by observing a previous withdrawal transaction on-chain (proofs are public in calldata)
-
Forge new proofs — For each withdrawal, keep
AandBthe same, compute a newC'that accounts for a different_nullifierHash -
Loop withdrawals — Each call uses a unique nullifier (the attacker used sequential values like
0xdead0000,0xdead0001, ...,0xdead001c) to bypass the double-spend check - Drain the pool — Repeat until empty
On Base alone, 29 consecutive forged withdrawals extracted 2.9 ETH (denomination was 0.1 ETH per withdrawal). Across both chains, the total reached approximately $2.26 million worth of FOOM tokens.
The attack was a copycat — the exact same vulnerability had been exploited earlier in Veil Cash, a similar privacy protocol. The attacker simply replicated the technique against FOOMCASH's identically misconfigured verifier.
Root Cause: A Missing CLI in Phase 2
FOOMCASH later disclosed that the misconfiguration occurred during the Phase 2 Trusted Setup. The trusted setup for Groth16 involves two phases:
- Phase 1 (Powers of Tau): Generates universal parameters
-
Phase 2 (Circuit-specific): Generates the circuit-specific proving and verification keys, including the distinct
γandδparameters
FOOMCASH stated that a "missing command-line interface in the Phase 2 Trusted Setup process" led to the delta2 and gamma2 parameters being set identically. In other words, the setup tool either skipped a step or was misconfigured, and nobody caught it.
This is particularly alarming because:
- The protocol had undergone a third-party audit
- They maintained a bug bounty program
- The vulnerability was a known pattern (already exploited in Veil Cash)
The White-Hat Save
In a somewhat redeeming turn, approximately $1.83 million (81%) of the stolen funds were recovered through white-hat operations:
- Duha (white-hat alias) identified the vulnerability and secured funds on Base — received a $320,000 bounty
- Decurity handled recovery on Ethereum — received a $100,000 bounty
This highlights an important dynamic in DeFi security: when vulnerabilities are this straightforward, the race between black-hats and white-hats becomes a pure speed competition.
Lessons for ZK Protocol Developers
1. Verify Your Verification Key
After deploying a Groth16 verifier, programmatically verify that:
gamma2 ≠ delta2-
alpha1andbeta2are non-trivial (not the identity element) - All parameters match the expected output of your trusted setup
This should be an automated deployment check, not a manual review.
# Pseudo-check after deployment
assert vk.gamma2 != vk.delta2, "CRITICAL: gamma2 == delta2 breaks soundness"
assert vk.alpha1 != G1.identity(), "alpha1 must not be identity"
assert vk.beta2 != G2.identity(), "beta2 must not be identity"
2. Audit the Setup, Not Just the Circuit
Most ZK audits focus on circuit constraints — checking for under-constrained variables, missing range checks, etc. But the FOOMCASH exploit wasn't a circuit bug. The circuit was fine. The setup was broken.
Audit scope for ZK protocols must include:
- The trusted setup ceremony procedure
- The deployed verification key parameters
- The deployment scripts that extract and set these parameters
3. Use Established Setup Tooling
The snarkjs library provides well-tested Phase 2 setup tooling. If your setup process requires custom CLI tools, that's a red flag. Stick to battle-tested ceremony tools:
# Standard Phase 2 with snarkjs
snarkjs groth16 setup circuit.r1cs powersOfTau.ptau circuit_0000.zkey
snarkjs zkey contribute circuit_0000.zkey circuit_final.zkey --name="Contributor"
snarkjs zkey export verificationkey circuit_final.zkey verification_key.json
4. Cross-Reference Known Vulnerabilities
The FOOMCASH exploit was a copycat of the Veil Cash hack. The gamma2 == delta2 pattern was already publicly documented. Yet FOOMCASH deployed with the same flaw.
Maintain awareness of:
- The 0xPARC ZK Bug Tracker on GitHub
- Published exploit analyses from BlockSec, Trail of Bits, and zkSecurity
- The growing body of ZK-specific vulnerability patterns
5. Consider Alternative Proof Systems
Groth16's trusted setup is its Achilles' heel. If your protocol can tolerate slightly larger proofs or longer verification times, consider:
- PLONK/KZG: Universal trusted setup (not circuit-specific)
- STARKs: No trusted setup at all (transparent)
- Halo2: Recursive proof composition without a trusted setup
The tradeoff is usually proof size and verification gas cost vs. setup complexity and risk.
The Bigger Picture: ZK Security Is Still Immature
We're in the early days of production ZK deployments. The tooling is improving, but the security surface is enormous:
| Vulnerability Class | Example |
|---|---|
| Under-constrained circuits | Missing range checks allow invalid witnesses |
| Trusted setup misconfig | gamma2 == delta2 (this exploit) |
| Frozen heart attacks | Forging proofs in certain proof systems |
| Prover-side leaks | Side channels revealing witness data |
| Verifier gas griefing | Malicious proofs that are expensive to reject |
| Upgrade key compromise | Admin can swap verification key post-deployment |
The FOOMCASH hack is a reminder that zero-knowledge doesn't mean zero-risk. The math is sound. The implementations are where things break.
Conclusion
The FOOMCASH exploit crystallizes a truth about smart contract security that extends beyond ZK: the most devastating vulnerabilities are often the simplest. Not a novel cryptographic attack. Not a breakthrough in computation. Just two parameters that should have been different — and weren't.
For teams building ZK protocols: your trusted setup is as critical as your circuit design. Verify your verification keys. Audit your deployment pipeline. And check the bug tracker before you deploy — because someone else's mistake might already be your vulnerability.
This analysis is based on publicly available incident reports, on-chain data, and technical writeups from BlockSec, Decurity, and the broader security research community.
Tags: #zkSNARK #DeFi #Security #Groth16 #SmartContracts #Ethereum #ZeroKnowledge #Exploit
Top comments (0)