FoomCash Exploit: How a Skipped Groth16 Setup Ceremony Enabled $2.26M in ZK Proof Forgery
On February 26, 2026, an attacker drained $2.26 million from FoomCash — a ZK-proof-powered privacy lottery protocol — by forging zkSNARK proofs that the on-chain verifier accepted as valid. The root cause was devastating in its simplicity: the team skipped a single CLI step during the Groth16 trusted setup ceremony, leaving the verifier's gamma2 and delta2 parameters set to the same default value. Security firm Decurity and white-hat hacker Duha raced to rescue $1.84 million (81%) before the attacker could extract it all.
What Is the Groth16 Trusted Setup Ceremony?
Groth16 is the most widely deployed zkSNARK proving system, used by protocols like Tornado Cash, Zcash, and dozens of privacy applications. It produces compact, constant-size proofs that verify in milliseconds. But it requires a trusted setup ceremony — a two-phase initialization process that generates the cryptographic parameters the verifier uses to check proofs.
Phase 1: Powers of Tau (Universal)
This phase generates universal parameters shared across all circuits. It is a community ceremony where multiple participants contribute randomness. As long as at least one participant destroys their secret, the ceremony is secure. This phase is well-understood, widely available, and typically uses pre-computed outputs from ceremonies like Hermez or Zcash.
Phase 2: Circuit-Specific Contribution (Critical)
This phase generates parameters unique to the specific circuit being deployed. It randomizes two critical values: gamma and delta. These parameters ensure that each proof is bound to a specific witness (the secret knowledge being proved). Without this step, gamma and delta remain set to the BN254 G2 generator — the default placeholder value in snarkjs.
How gamma2 == delta2 Breaks Everything
The Groth16 verification equation is a bilinear pairing check:
// Groth16 Verification Equation
e(A, B) == e(alpha1, beta2) * e(vk_x, gamma2) * e(C, delta2)
// Where:
// A, B, C = proof elements submitted by the prover
// alpha1, beta2 = fixed setup parameters
// vk_x = linear combination of public inputs and verification key
// gamma2, delta2 = MUST be independent random values
When gamma2 == delta2, the two right-hand pairing terms collapse:
// BROKEN: When gamma2 == delta2, the equation simplifies
e(vk_x, gamma2) * e(C, delta2)
= e(vk_x, gamma2) * e(C, gamma2) // delta2 replaced with gamma2
= e(vk_x + C, gamma2) // bilinear pairing property
// An attacker simply sets C = -vk_x to cancel both terms
// Then sets A = alpha1, B = beta2 to cancel the left side
// Result: e(alpha1, beta2) == e(alpha1, beta2) * e(0, gamma2)
// = e(alpha1, beta2) * 1
// = e(alpha1, beta2) ... ALWAYS TRUE
The attacker does not need a valid witness. They do not need to know any secret. They compute C = -vk_x using publicly available verification key constants and submit a forged proof that passes verification for any arbitrary nullifierHash.
The Attack Flow
The Vulnerable Verifier Contract
The on-chain verifier deployed by FoomCash contained no validation that the trusted setup was properly completed:
// VULNERABLE - Deployed with gamma2 == delta2 (both G2 generator)
contract Verifier {
// Setup parameters baked into the contract
G1Point alpha1;
G2Point beta2;
G2Point gamma2; // G2 generator (Phase 2 never randomized this)
G2Point delta2; // G2 generator (identical to gamma2)
function verifyProof(
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c,
uint[] memory input
) public view returns (bool) {
// Pairing check passes for ANY forged proof
// when gamma2 == delta2
return pairing(a, b, alpha1, beta2, vk_x, gamma2, c, delta2);
}
}
The Safe Pattern: Constructor Validation
// SAFE - Validate setup parameters at deployment
contract Verifier {
constructor(
G1Point memory _alpha1,
G2Point memory _beta2,
G2Point memory _gamma2,
G2Point memory _delta2,
G1Point[] memory _ic
) {
// CRITICAL: Reject deployments where gamma2 == delta2
require(
_gamma2.X[0] != _delta2.X[0] ||
_gamma2.X[1] != _delta2.X[1] ||
_gamma2.Y[0] != _delta2.Y[0] ||
_gamma2.Y[1] != _delta2.Y[1],
"INVALID_SETUP: gamma2 == delta2"
);
alpha1 = _alpha1;
beta2 = _beta2;
gamma2 = _gamma2;
delta2 = _delta2;
ic = _ic;
}
}
The Attack in Numbers
| Metric | Value |
|---|---|
| Total drained | $2.26 million (24.28 trillion FOOM tokens) |
| White-hat recovery | $1.84 million (81%) |
| Net protocol loss | ~$420,000 |
| Chains affected | Ethereum + Base |
| Base loss (malicious) | ~$427,000 |
| Ethereum (rescued) | ~$1.84 million |
| Bounty to Duha | $320,000 |
| Security fee to Decurity | $100,000 |
| Root cause | Skipped snarkjs Phase 2 trusted setup |
| Time from Veil Cash disclosure to copycat | ~48 hours |
The Copycat Problem: Veil Cash to FoomCash in 48 Hours
Two days before the FoomCash exploit, the same gamma2 == delta2 flaw was discovered and publicly disclosed in Veil Cash, a smaller protocol on Base that lost only ~2.9 ETH. The Veil Cash post-mortem explained the exact vulnerability in detail, including the mathematical proof of how to forge verification.
Within 48 hours, someone applied the identical technique to FoomCash's larger liquidity pools ($8M+ TVL). BlockSec Phalcon flagged the attack as a direct copycat. This timeline — public disclosure to copycat exploitation in under two days — represents a critical risk window for any ZK protocol sharing the same vulnerability class.
Why This Keeps Happening
ZK proof system deployment failures persist because of a fundamental mismatch between the cryptographic complexity of these systems and the operational practices of teams deploying them:
- Ceremony complexity — The Groth16 setup involves multiple CLI steps (Powers of Tau, Phase 2 contribution, verification key export). Skipping or misordering any step can be catastrophic, but the tools do not always enforce correctness.
- No on-chain validation — Standard snarkjs-generated verifier contracts do not check whether gamma2 equals delta2. The contract compiles and deploys successfully even with broken parameters.
- Fork-and-deploy culture — Many privacy protocols fork existing codebases (Tornado Cash, etc.) and modify circuits without re-running the full setup ceremony. FoomCash marketed itself as an evolution of Tornado Cash with added lottery mechanics.
- Abandoned monitoring — FoomCash's social accounts went silent in November 2025, three months before the breach. No one was watching the deployed contracts.
- Audit gaps — The cryptographic setup process itself is rarely included in smart contract audit scope. Auditors review Solidity code but may not verify that the trusted setup ceremony was completed correctly.
How to Protect Your Protocol
1. Validate Setup Parameters at Deployment
Add a constructor check that gamma2 != delta2. This single require statement would have prevented the entire FoomCash exploit. It costs minimal gas at deployment and provides permanent protection.
2. Complete the Full Trusted Setup Ceremony
For Groth16 circuits, the full snarkjs workflow is: snarkjs powersoftau (Phase 1) followed by snarkjs zkey new, snarkjs zkey contribute (Phase 2), and snarkjs zkey verify. Never skip the verification step. Document every phase with cryptographic hashes of intermediate outputs.
3. Add Withdrawal Rate Limiting
Even with a valid setup, implement circuit breakers: maximum withdrawals per block, per-address cooldowns, and anomalous-outflow detection. These defense-in-depth measures buy time for white-hat intervention.
4. Audit the Cryptographic Setup, Not Just the Code
Smart contract audits must include verification of trusted setup parameters. Check that gamma2 and delta2 are distinct, that verification keys match the deployed circuit, and that Phase 2 contributions are properly recorded.
5. Continuous Monitoring
Deploy real-time monitoring for unusual withdrawal patterns. FoomCash's team had been absent for three months. Active monitoring by Decurity and BlockSec Phalcon is what enabled the $1.84M rescue.
Audit Your ZK Protocol
Our security team reviews smart contracts, verifier configurations, and cryptographic setup parameters. We check for gamma2/delta2 mismatches, incomplete ceremonies, and 20+ other vulnerability patterns.
Free Vulnerability Scan Request Full AuditFree scan requires no signup. Full audits include trusted setup verification.
On-Chain Evidence
| Component | Address / Hash |
|---|---|
| Victim Contract (Ethereum) | 0x239af915abcd0a5dcb8566e863088423831951f8 |
| Victim Contract (Base) | 0xdb203504ba1fea79164af3ceffba88c59ee8aafd |
| Broken Verifier (Ethereum) | 0xc043865fb4D542E2bc5ed5Ed9A2F0939965671A6 |
| Broken Verifier (Base) | 0x02c30D32A92a3C338bc43b78933D293dED4f68C6 |
| Attacker | 0x73f55A95D6959D95B3f3f11dDd268ec502dAB1Ea |
| Exploit Contract | 0x005299b37703511b35d851e17dd8d4615e8a2c9b |
| Attack Tx (Ethereum) | 0xce20448233f5ea6b6d7209cc40b4dc27b65e07728f2cbbfeb29fc0814e275e48 |
| Attack Tx (Base) | 0xa88317a105155b464118431ce1073d272d8b43e87aba528a24b62075e48d929d |
| White-Hat Rescue | whitehat-rescue.eth (Decurity) |
Timeline
- November 2025 — FoomCash social accounts go silent; team abandons active monitoring
- February 24, 2026 — Veil Cash exploited via identical gamma2 == delta2 flaw (~2.9 ETH lost); post-mortem published
- February 26, 2026 — Attacker drains FoomCash Base contract ($427K) in a single transaction
- February 26, 2026 — Decurity and Duha independently identify the flaw, front-run the attacker on Ethereum, and secure $1.84M
- February 26, 2026 — BlockSec Phalcon flags the exploit as a Veil Cash copycat attack
- March 1, 2026 — FoomCash acknowledges the exploit; pays $320K bounty to Duha and $100K security fee to Decurity
- March 4, 2026 — Rekt.news publishes full post-mortem: "The Unfinished Proof"