# Foom.cash $1.84M Whitehat Rescue: Groth16 Trusted Setup Exploit

> Foom.cash's ZK lottery shipped a Groth16 verifier with an unfinished trusted setup, making every withdrawal proof forgeable. Defimon detected the exploit and preemptively rescued $1.84M from the Ethereum deployment.

Source: https://defimon.xyz/blog/foom-cash-whitehat-rescue-february-2026 · Published: 2026-02-26 · Network: Ethereum · Impact: $1.84M

---

## TLDR

On February 26, 2026, Foom.cash, a zero-knowledge lottery and mixer deployed on Ethereum and Base, was found to have shipped a Groth16 verifier whose trusted setup was never finished. Its `gamma` and `delta` parameters were both left at their placeholder value, the BN254 G2 generator. With the two parameters equal, any withdrawal proof could be forged with no knowledge of a valid witness, making every deposit in the contract freely drainable by anyone who read the public verifying key. The flaw was discovered and exploited by the whitehat researcher duha_real, who drained the Base deployment under the terms of Foom.cash's public bug bounty. Once that drain put the forgery on-chain, the same technique was reproducible by anyone against the unprotected Ethereum deployment. Defimon detected the exploit in real time and preemptively drained the Ethereum contract into a whitehat address before a malicious actor could, then returned approximately $1.84 million to the project the same day. This is a writeup of the vulnerability and the rescue.

## Technical Analysis

Foom.cash used Groth16 proofs to authorize withdrawals. A user deposits, then later calls `collect()` with a proof that they own an unspent note, without revealing which deposit is theirs. The on-chain Groth16 verifier is the only thing standing between a caller and the pooled funds, so its soundness is the entire security model.

Groth16 verification reduces to a single pairing product equation. The snarkjs-generated verifier checks:

```solidity
// Groth16Verifier.verifyProof, simplified
// vk_x = IC[0] + sum(input[i] * IC[i])
// accept iff:
//   e(-A, B) * e(alpha, beta) * e(vk_x, gamma) * e(C, delta) == 1
```

Here `alpha`, `beta`, `gamma`, `delta` and the `IC` points are fixed in the verifying key and readable by anyone. `A`, `B`, `C` are the proof, and `vk_x` is a linear combination of the public inputs with the `IC` points. Soundness depends on `gamma` and `delta` being independent values whose discrete logs (the "toxic waste") were destroyed during the trusted setup ceremony.

The ceremony was never completed. In snarkjs, the Phase 2 setup is initialized with both parameters set to the group generator as a placeholder, expecting later contributions to randomize `delta` while `gamma` stays fixed:

```javascript
// snarkjs zkey init — placeholders, meant to be replaced in Phase 2
const bg2 = new Uint8Array(sG2);
curve.G2.toRprLEM(bg2, 0, curve.G2.g); // G2 generator
await fdZKey.write(bg2); // gamma2 = G2 generator
await fdZKey.write(bg1); // delta1 = G1 generator
await fdZKey.write(bg2); // delta2 = G2 generator  <-- must be randomized
```

Foom.cash deployed verifiers built straight from this initialized-but-uncontributed key. The command that draws a random `delta` was never run, so the production verifier carried `delta == gamma == G2` generator.

With the two parameters equal, the last two terms of the verification equation collapse. Because `gamma` and `delta` are the same point, the pairings over them can be merged:

```text
e(vk_x, gamma) * e(C, delta)  =  e(vk_x, G2) * e(C, G2)  =  e(vk_x + C, G2)
```

Setting `C = -vk_x` sends that combined term to the identity:

```text
e(vk_x + (-vk_x), G2)  =  e(O, G2)  =  1
```

That removes the public input and the `C` term entirely. What remains is `e(-A, B) * e(alpha, beta) == 1`, which is satisfied by reading `alpha` and `beta` straight from the verifying key and setting `A = alpha`, `B = beta`:

```text
e(-alpha, beta) * e(alpha, beta)  =  1
```

The forged proof is therefore `(A = alpha, B = beta, C = -vk_x)`. Every value in it comes from the public verifying key and the public inputs. No deposit, no note, and no secret witness are required. An attacker recomputes `vk_x` for each call with an incrementing nullifier and loops `collect()` until the contract is empty.

## Impact Assessment

The flaw affected both deployments. Across Ethereum and Base, roughly $2.26 million in FOOM was drainable, backed by a liquidity pool of over five million dollars on Ethereum. Because the forgery needs nothing but the on-chain verifying key, the funds were exposed to any observer capable of reading the contract, not to a privileged few. This is the failure mode of a broken trusted setup: it does not degrade security, it removes it, and it does so silently because the verifier still accepts every legitimate proof exactly as before.

The Ethereum deployment held the larger balance, approximately $1.84 million, sitting behind the verifier at `0xc043865fb4D542E2bc5ed5Ed9A2F0939965671A6`.

## Response and Recovery

The vulnerability was found and exploited by the whitehat researcher duha_real, who drained the Base deployment under the terms of Foom.cash's public bug bounty on Bitcointalk, whose only stated rule was the code. That drain put the forgery on-chain in full view, and the Ethereum deployment, holding roughly $1.84 million, ran the same unfinished verifier with no bounty and no protection standing between its balance and the first person to copy the technique.

Defimon detected the exploit as it happened and launched a preemptive whitehat rescue. Using the same forged-proof technique, we drained the Ethereum lottery contract into the whitehat-rescue.eth address in a single sequence of `collect()` calls, securing the full balance before a malicious actor could reproduce it, then returned approximately $1.84 million to the project the same day at 10:11 UTC. Foom.cash offered a security fee, which we accepted, and published a post-mortem on March 1, 2026 confirming the root cause as the unfinished Phase 2 setup.

Trusted-setup failures are not exploitable by flash loans or oracle games, so they never appear in the usual monitoring signatures, yet the drain that follows looks identical on-chain to any other pool being emptied: an address with no deposit history pulling the full balance out in a tight loop. That is the signal [Defimon](/) fired on here, and the reason our feed watches for that shape of drain across major chains. When the same public exploit is available to rescuer and thief alike, the outcome comes down to who acts first, which is why teams wire an [automated incident-response pipeline](/blog/automating-defi-incident-response) to a detection feed rather than to a human refreshing a block explorer.

**Related Addresses:**

- Ethereum lottery (victim): [0x239af915abcd0a5dcb8566e863088423831951f8](https://etherscan.io/address/0x239af915abcd0a5dcb8566e863088423831951f8)
- Groth16 verifier: [0xc043865fb4D542E2bc5ed5Ed9A2F0939965671A6](https://etherscan.io/address/0xc043865fb4D542E2bc5ed5Ed9A2F0939965671A6)
- whitehat-rescue.eth (Decurity): [0x46c403e3DcAF219D9D4De167cCc4e0dd8E81Eb72](https://etherscan.io/address/0x46c403e3DcAF219D9D4De167cCc4e0dd8E81Eb72)
- Rescue drain transaction: [0xce20448233f5ea6b6d7209cc40b4dc27b65e07728f2cbbfeb29fc0814e275e48](https://etherscan.io/tx/0xce20448233f5ea6b6d7209cc40b4dc27b65e07728f2cbbfeb29fc0814e275e48)
- Return to project: [0xfed2dc60634b321fab073312168b327a47fdd76d2a39489249e4ef986671e83f](https://etherscan.io/tx/0xfed2dc60634b321fab073312168b327a47fdd76d2a39489249e4ef986671e83f)

