TL;DR
On March 2, 2026, attackers drained $2.26M from Foom Cash — a zk-SNARK privacy protocol on Ethereum and Base — by forging zero-knowledge proofs. The root cause? A single omitted step during the Groth16 trusted setup that left γ and δ parameters at their default generator values, collapsing the entire soundness guarantee of the proof system.
A white-hat rescue recovered $1.84M (81%) through front-running the attacker with the same forged-proof technique. This article dissects the vulnerability, the proof malleability exploit, and what every team deploying ZK circuits needs to learn from it.
The Attack Timeline
Feb 27, 2026 — First suspicious withdrawals detected on Base
Mar 02, 2026 — Full-scale exploit: 24.28T FOOM tokens drained (~$2.26M)
Mar 02, 2026 — White-hat @duha_real front-runs attacker on Base ($1.84M saved)
Mar 02, 2026 — DecurityHQ rescues Ethereum mainnet funds
Mar 03, 2026 — Foom Cash patches verifier, awards $420K in bounties
The attack wasn't a zero-day in cryptography. It was a deployment error that turned battle-tested math into a rubber stamp.
How Groth16 Proofs Work (The 60-Second Version)
Groth16 is the most widely deployed zk-SNARK proving system in DeFi — used by Tornado Cash, Zcash, and dozens of privacy protocols. Here's the simplified flow:
- Trusted Setup generates public parameters (CRS) including special values γ (gamma) and δ (delta)
- Prover creates a proof π = (A, B, C) demonstrating knowledge of a secret without revealing it
-
Verifier checks:
e(A, B) = e(α, β) · e(∑ᵢ aᵢuᵢ/γ, γ) · e(C, δ)
The security hinges on γ and δ being random, secret values generated during the trusted setup ceremony. If they default to the G2 generator point, the pairing equation degenerates — any proof validates.
The Root Cause: A Skipped CLI Command
During Foom Cash's Phase 2 trusted setup, the team used snarkjs to generate proving keys. The ceremony requires running:
# Phase 2 contribution — THIS STEP WAS SKIPPED
snarkjs powersoftau contribute pot12_0001.ptau pot12_0002.ptau \
--name="Second contribution" -v
Without this step, γ and δ remain at their default values — the G2 generator point G₂. This means:
γ = G₂ (should be random)
δ = G₂ (should be random)
When γ = δ = G₂, the verification equation collapses:
e(A, B) = e(α, β) · e(∑ᵢ aᵢuᵢ, G₂) · e(C, G₂)
The "randomized" terms become deterministic. An attacker who knows any valid proof can algebraically derive new valid proofs for arbitrary inputs.
The Exploit: Proof Malleability at Scale
Step 1: Observe a Valid Withdrawal
The attacker monitored Foom Cash's withdraw() transactions and captured a legitimate proof π = (A, B, C) along with its public inputs.
Step 2: Forge New Proofs
With γ = δ = G₂, proof components can be manipulated:
A' = A + r·G₁ # Shift A by random scalar r
B' = B # Keep B unchanged
C' = C + r·β/δ·G₁ # Adjust C to maintain pairing equation
Since δ = G₂, the division β/δ is trivially computable. The forged proof (A', B', C') passes verification for different public inputs — specifically, the attacker's own withdrawal address and a fabricated nullifier.
Step 3: Drain via Loop
// Simplified attack loop
for (uint i = 0; i < N; i++) {
// Each iteration uses a freshly forged proof
foomCash.withdraw(
forgedProof, // Malleable proof accepted by broken verifier
attackerAddress, // Attacker's withdrawal destination
fakeNullifier, // New nullifier each time (bypass replay protection)
fakeRoot // Accepted because verifier is broken
);
}
The attacker executed this loop across both Base and Ethereum, extracting 24.28 trillion FOOM tokens.
Step 4: Liquidate
Tokens were swapped through DEX aggregators for ETH, then partially bridged and mixed.
The White-Hat Rescue: Fighting Fire With Fire
Security researcher @duha_real identified the exploit transactions on Base and reverse-engineered the proof forgery technique. The rescue strategy was elegant:
- Reproduce the same proof malleability exploit
- Submit withdrawal transactions with higher gas fees (front-running)
- Redirect at-risk funds to a secure multisig before the attacker could claim them
This "white-hat front-running" recovered $1.84M on Base. DecurityHQ replicated the approach on Ethereum mainnet. Total bounties awarded: $420K.
Why This Wasn't Caught
1. Trusted Setup Verification Is Manual
Most teams treat the trusted setup as a one-time ceremony and never re-verify the output parameters. There's no standard CI check that validates γ ≠ G₂ and δ ≠ G₂.
2. Tests Pass With Broken Parameters
Unit tests that verify "proof A validates" will still pass even with default parameters — because the prover and verifier use the same broken CRS. The tests can't detect that any proof validates.
3. The Veil Cash Precedent Was Ignored
An identical vulnerability was exploited in Veil Cash just days earlier. The Foom Cash team either didn't monitor security feeds or didn't recognize the shared architecture risk.
Defense Patterns for ZK Protocol Teams
1. Automated CRS Validation
# Post-setup verification script
from py_ecc.bn128 import G2, multiply, eq
def validate_crs(gamma, delta):
"""Ensure trusted setup parameters aren't default generator values"""
assert not eq(gamma, G2), "CRITICAL: gamma == G2 generator!"
assert not eq(delta, G2), "CRITICAL: delta == G2 generator!"
assert not eq(gamma, delta), "WARNING: gamma == delta (low entropy)"
print("✓ CRS parameters appear properly randomized")
# Run this in CI after every trusted setup ceremony
2. Multi-Party Ceremony With Verification
Never run a single-party trusted setup for production. Use:
snarkjswith ≥ 3 independent contributors- Public verification transcripts (Hermez-style)
- Automated checks between each contribution phase
3. Proof Uniqueness Enforcement
Even with correct parameters, add on-chain proof uniqueness checks:
mapping(bytes32 => bool) public usedProofs;
function withdraw(Proof calldata proof, ...) external {
bytes32 proofHash = keccak256(abi.encode(proof.a, proof.b, proof.c));
require(!usedProofs[proofHash], "Proof already used");
usedProofs[proofHash] = true;
// ... verify and process
}
4. Circuit-Level Binding
Bind proofs to specific transaction data (msg.sender, block.number) within the circuit itself — not just in the contract wrapper. This prevents proof transplantation even if the verifier is compromised.
5. Real-Time Monitoring
Deploy anomaly detection for:
- Withdrawal volume exceeding deposit volume
- Multiple withdrawals from the same nullifier pattern
- Proof verification gas costs deviating from baseline
The Bigger Picture: ZK Security Is Infrastructure Security
The Foom Cash exploit isn't about broken cryptography — Groth16 remains sound. It's about the deployment pipeline around cryptographic primitives. The same class of error can appear in:
| Protocol Type | Analogous Risk |
|---|---|
| ZK-Rollups | Misconfigured verifier on L1 settlement contract |
| Privacy Mixers | Trusted setup ceremony with insufficient contributions |
| ZK-Bridges | Proof relay accepting malleable proofs cross-chain |
| ZK-Identity | Credential proofs forgeable due to setup errors |
As ZK infrastructure expands across DeFi, the attack surface isn't the math — it's the ceremony, deployment, and verification pipeline around the math.
Key Takeaways
- One skipped CLI step collapsed a $2.3M protocol's security — trusted setup ceremonies need automated validation
- Proof malleability from default parameters let attackers forge unlimited valid proofs
- White-hat front-running recovered 81% of stolen funds — rapid response matters
- Unit tests won't catch this — you need CRS parameter validation as a separate security check
- The Veil Cash precedent was public — monitoring competitor/peer exploits is a security function, not optional
This analysis is part of the DreamWork Security research series on DeFi vulnerability patterns. Follow for weekly deep-dives into real-world exploits and defense strategies.
Disclaimer: This article is for educational purposes only. The technical details are reconstructed from public incident reports, on-chain data, and security researcher disclosures.
Top comments (0)