DEV Community

ohmygod
ohmygod

Posted on

Forging the Unforgivable: How a zkSNARK Verification Key Misconfiguration Drained $2.26M from FOOMCASH

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:

  1. Nullifier tracking — Each withdrawal uses a unique nullifier hash to prevent double-spending
  2. 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, δ)
Enter fullscreen mode Exit fullscreen mode

Where:

  • e() is a bilinear pairing function
  • A, B, C are the proof elements
  • α, β, γ, δ are verification key parameters from the trusted setup
  • Lᵢ are precomputed values for each public input aᵢ

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, γ)
Enter fullscreen mode Exit fullscreen mode

This can be simplified to:

e(A, B) = e(α, β) · e(∑ aᵢ·Lᵢ + C, γ)
Enter fullscreen mode Exit fullscreen mode

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ᵢ
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

The attack steps:

  1. Obtain one valid proof — Either from a legitimate deposit or by observing a previous withdrawal transaction on-chain (proofs are public in calldata)
  2. Forge new proofs — For each withdrawal, keep A and B the same, compute a new C' that accounts for a different _nullifierHash
  3. Loop withdrawals — Each call uses a unique nullifier (the attacker used sequential values like 0xdead0000, 0xdead0001, ..., 0xdead001c) to bypass the double-spend check
  4. 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
  • alpha1 and beta2 are 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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)