Foom Cash Groth16 delta == gamma setup collapse / whitehat rescue (2026-02-26)
- Name: Foom Cash Groth16
delta == gammasetup collapse / whitehat rescue - Date: 2026-02-26
- Protocol: Foom Cash
- Chain: Ethereum, Base
- Category: verification-key independence collapse / unfinalized Groth16 setup / forged proof claim near miss
- Loss: essentially the full reachable balances on Base and Ethereum were vulnerable; the public PoC measured
4.588196709631799956807157941481e30token units drained on Base and1.9695576757802192910518134117126e31token units drained on Ethereum, while public reporting valued the at-risk balances at about$2.26M; the documented drains were whitehat rescues rather than malicious theft - Summary: Foom Cash, a lottery-style application that used Groth16 proofs to authorize prize collection, deployed the same kind of unsound verifier-key state later seen in Veil Cash:
gammaanddeltain the verification key were both equal to the BN254G2generator. That broke Groth16 soundness at the verifier-key level. Whitehats on Base and Ethereum independently used the resulting forged-proof construction to rescue nearly all reachable funds before a malicious actor did. The application-specific exploit surface is worth preserving: Foom’scollect()path accepted attacker-chosen public inputs for_root,_nullifierHash,_rewardbits,_recipient,_relayer,_fee, and_refund, then computed a prize from_rewardbitsand treated verifier success as authority to transfer the reward token. OnceverifyProof()became a tautology, the whitehats could choose accepted claim parameters like_rewardbits = 7, computevk_x, setA = alpha,B = beta,C = -vk_x, and drain the live token balances with repeated fresh nullifiers even though no valid winning witness existed. - Trigger / entry point:
- The vulnerable entry point was Foom’s
collect()function on its lottery contracts, which used a Groth16 proof to authorize reward claims. collect()marked the supplied nullifier as spent, requiredmsg.value == _refund, computedrewardfrombetMinand the caller-chosen_rewardbits, checked_rootmembership in the stored roots set, then calledwithdraw.verifyProof(_pA, _pB, _pC, [_root, _nullifierHash, _rewardbits, uint160(_recipient), uint160(_relayer), _fee, _refund])before transferring reward value.- On Base, the relevant pair was verifier
0x02c30D32A92a3C338bc43b78933D293dED4f68C6and lottery0xdb203504ba1fea79164AF3CeFFBA88C59Ee8aAfD. - On Ethereum, the relevant pair was verifier
0xc043865fb4D542E2bc5ed5Ed9A2F0939965671A6and lottery0x239AF915abcD0a5DCB8566e863088423831951f8. - Both deployed verifiers embedded a malformed key where
gamma == delta == G2 generator.
- The vulnerable entry point was Foom’s
- Root cause:
- The root failure sat in verifier-key generation or deployment, not in a bespoke Solidity bug inside
collect(). - Groth16 depends on
gammaanddeltabeing independent ceremony outputs. Foom’s deployed keys instead made them identical and public, which collapses the pairing equation into one an attacker can satisfy with public key material alone. - Public analysis tied that exact
gamma == delta == G2 generatorshape to the canonicalsnarkjsPhase-2 omission failure mode:groth16 setupcreates the initial*_0000.zkeywith bothgamma2anddelta2written as the BN254G2generator, and only the laterzkey contributestep randomizesdelta. Exporting verifier material from the pre-contribution artifact therefore preserves the tautological verifier state directly in the onchain key. - Foom’s application layer then turned verifier success directly into economic authority by letting
collect()pay real reward value once the proof checked out. - The combination meant any attacker who recognized the malformed verifier could forge claims with arbitrary accepted public inputs until the contract balance was exhausted.
- The root failure sat in verifier-key generation or deployment, not in a bespoke Solidity bug inside
- Exploit path:
- Reviewers noticed that both Foom deployments used verifier keys where
gammaanddeltawere not independent, but both equal to the BN254G2generator. - Whitehats recognized that this made proof verification tautological under the standard forged-proof construction: set
A = alpha,B = beta, computevk_xfrom the chosen public inputs and contractIC[]points, then setC = -vk_x. - On Base, whitehat
@duha_realdeployed rescue contract0x005299B37703511B35D851e17dd8D4615e8A2C9Band executed rescue transaction0xa88317a105155b464118431ce1073d272d8b43e87aba528a24b62075e48d929dat block42,650,620. - On Ethereum, a second independent whitehat,
whitehat-rescue.eth, deployed rescue contract0x256a5D6852Fa5B3C55D3b132e3669A0bdE42e22cand executed rescue transaction0xce20448233f5ea6b6d7209cc40b4dc27b65e07728f2cbbfeb29fc0814e275e48at block24,539,648. - In each rescue, the caller supplied attacker-chosen public inputs for
_root,_nullifierHash,_rewardbits,_recipient,_relayer,_fee, and_refundthat satisfied the contract’s ordinary parameter checks; the published PoC path used_rewardbits = 7,_relayer = address(0),_fee = 0, and_refund = 0. - The forged proof passed because the broken verifier key no longer required knowledge of a real witness or winning note.
- Repeating the process with fresh nullifiers let the whitehats claim nearly all reachable balances:
10iterations on Base using nullifiers3735879680through3735879689, and30iterations on Ethereum using nullifiers99999990000through99999990029. - The Base rescue drained token
0x02300aC24838570012027E0A90D3FEcCEF3c51d2from4.589254196734797608386036919841e30units down to1.05748710299765157887897836e27units, while the Ethereum rescue drained token0xd0D56273290D339aaF1417D9bfa1bb8cFe8A0933from1.9695576810020236864e31units down to5.2218043953481865882874e22units. - The end state was not a realized malicious theft but a clean rescue of a proof-authorized claim surface that could have been emptied by anyone who noticed the malformed verifier first.
- Reviewers noticed that both Foom deployments used verifier keys where
- What the attacker needed to be true:
- The deployed verification keys on both chains had to preserve the
gamma == deltadegeneracy. collect()had to trust verifier success directly for claim authorization.- The contract’s non-proof checks had to be satisfiable with attacker-chosen public inputs, including known roots and fresh nullifiers.
- The attacker needed enough room inside Foom’s payout rules to convert forged claim success into real token withdrawals.
- Reachable balances had to still sit in the contracts when the issue became known.
- The deployed verification keys on both chains had to preserve the
- Technical anatomy worth remembering:
- Foom is the useful companion to Veil because the application surface was different. Veil used fixed-denomination privacy withdrawals; Foom used lottery prize collection with configurable
_rewardbits,_fee,_recipient, and reward-token transfer semantics. - The proof forgery was still the same core algebra: the verifier key itself was broken, so the application’s proof gate became a formality.
- The public-input shape matters. The PoC notes that
IC[5],IC[6], andIC[7]were multiplied by zero in the whitehat path because_relayer,_fee, and_refundwere all zero, so the real leverage sat in choosing a live root, a fresh nullifier, maximal reward bits, and the payout recipient. - The incident also shows how one malformed verifier artifact can replicate across chains, multiplying blast radius without changing the exploit mechanics. Here the Base and Ethereum deployments were not merely
same bug class; they carried the same recognizable artifact-family fingerprint and therefore should have entered one shared emergency sweep the moment either verifier was identified. - The more reusable lifecycle lesson is that the bad verifier had a recognizable family fingerprint, not just a one-off bug:
gamma == delta == G2 generatoris exactly the kind of artifact-class signal that should trigger sibling-deployment quarantine rather than chain-by-chain triage. - Because the observed drains were whitehat rescues, Foom is best preserved as a near miss and design lesson rather than only as a realized theft statistic.
- Foom is the useful companion to Veil because the application surface was different. Veil used fixed-denomination privacy withdrawals; Foom used lottery prize collection with configurable
Three proof-authority promotions this incident collapsed
-
A verifier artifact family existed at all
- Foom had live Groth16 verifier deployments on both Base and Ethereum, each with apparently normal proof-facing constants and a lottery contract wired to trust them.
- That first fact only established
there are verifier-shaped objects onchain, notthose objects came from a finalized sound ceremonyor eventhese two deployments are safely distinct.
-
That artifact family deserved to count as canonical winning-proof truth for
collect()- Foom then promoted those malformed verifiers into proof truth for
_root,_nullifierHash,_rewardbits,_recipient,_relayer,_fee, and_refundeven thoughgamma == delta == G2 generatormeant the keys no longer enforced knowledge of a real winning witness. - Once that second promotion failed,
collect()stopped provingthis claimant really owns a valid prize-bearing noteand started proving onlythese public inputs fit a verifier whose pairing equation has collapsed.
- Foom then promoted those malformed verifiers into proof truth for
-
That accepted proof truth deserved cross-chain reward-transfer authority
- The application still let verifier success directly authorize reward-token transfers on both chains, so a single malformed verifier family carried real payout authority over every funded sibling deployment.
- That is why the right response object was one family-wide rescue and reachability sweep rather than two chain-specific postmortems: the same broken proof gate was still allowed to cash out live balances wherever it had been copied.
- Representative transactions and addresses:
- Base whitehat tx:
0xa88317a105155b464118431ce1073d272d8b43e87aba528a24b62075e48d929d - Ethereum whitehat tx:
0xce20448233f5ea6b6d7209cc40b4dc27b65e07728f2cbbfeb29fc0814e275e48 - Base verifier:
0x02c30D32A92a3C338bc43b78933D293dED4f68C6 - Base lottery:
0xdb203504ba1fea79164AF3CeFFBA88C59Ee8aAfD - Base whitehat contract:
0x005299B37703511B35D851e17dd8D4615e8A2C9B - Base recipient:
0x73f55A95D6959D95B3f3f11dDd268ec502dAB1Ea - Base drained token:
0x02300aC24838570012027E0A90D3FEcCEF3c51d2 - Ethereum verifier:
0xc043865fb4D542E2bc5ed5Ed9A2F0939965671A6 - Ethereum lottery:
0x239AF915abcD0a5DCB8566e863088423831951f8 - Ethereum whitehat contract:
0x256a5D6852Fa5B3C55D3b132e3669A0bdE42e22c - Ethereum whitehat rescuer / recipient:
0x46c403e3DcAF219D9D4De167cCc4e0dd8E81Eb72 - Ethereum drained token:
0xd0D56273290D339aaF1417D9bfa1bb8cFe8A0933
- Base whitehat tx:
- Response / mitigation:
- Whitehats drained the vulnerable balances rather than leaving the malformed verifier live against funded contracts.
- Public analysis tied the flaw to setup or verifier-key generation hygiene rather than to a bespoke claim-path arithmetic bug.
- The correct remediation was to regenerate the verifier from a completed ceremony, verify deployed constants against finalized artifacts, and treat cross-chain sibling deployments as equally compromised until proven otherwise.
- In
snarkjsterms, teams should treat any verifier exported from a zero-contribution*_0000.zkeyor equivalent pre-finalization artifact as a release blocker, not a deployable key. - The more durable operational fix is family-level quarantine: once one live verifier fingerprints as
gamma == delta == G2 generator, every sibling deployment that shares that artifact lineage should move onto one emergency reachability sheet until each contract, entrypoint, and funded balance is either rescued or proven clean. - That grouping key should be the deployed verifier family itself — exported verification-key hash, full constant tuple, or another reproducible lineage fingerprint — not product branding or
same team, maybe same bugintuition. In Foom, the useful first question waswhich funded contracts carry this exact malformed verifier family?, notwhich frontend should we inspect next?
- zkSecurity’s follow-on response after the Veil / Foom cluster made the right operational move explicit: turn the fingerprint into a chain-wide hunting rule and sweep for verifiers that stored the same
G2element twice, instead of treating each compromised deployment as an isolated postmortem.
Pattern tags
- verification-key-independence-collapse
- verification-key-statement-inventory-and-asset-reachability-discipline
- trusted-setup-finalization-and-verifier-family-quarantine
- One quarantined verifier-artifact fingerprint should have merged Base and Ethereum into one shared rescue and reachability object before either deployment was touched.
Why the exploit worked
- Foom’s
collect()path let verifier success directly authorize reward-token transfers, so once the verifier key stopped being sound the application had no stronger proof-of-winning boundary left. - The malformed keys on both chains supported the same public-key-only forgery, letting whitehats choose acceptable public inputs, compute
vk_x, and satisfyverifyProof()without any real witness or winning ticket. - Foom’s application-specific public-input surface was still monetizable under forgery: known roots, fresh nullifiers, attacker-chosen recipients, and
_rewardbits = 7were enough to pull large reward transfers once the proof gate collapsed. - The same artifact-family fingerprint had already widened blast radius across two live deployments, which means the operational failure was not only
bad verifier on one appbutcopied bad verifier lineage stayed funded on multiple chains at once.
Detection and audit pivots
- Recompute the exact deployed verification-key structure on every funded chain, not just one canonical deployment: if
gammaanddeltaare equal or generator-valued anywhere, treat the whole lineage as suspect immediately. - Hash or otherwise fingerprint the whole deployed verifier family, not only the anecdotal
gamma == deltasymptom. Responders should be able to group siblings by exported-key hash, full constant tuple, or another reproducible lineage marker before they start chain-by-chain archaeology. - Test the real claim path, not only the cryptography in isolation: can a reviewer choose accepted
_root,_nullifierHash,_rewardbits, recipient, fee, and refund values and still forcecollect()to pay out with a forged proof? - Build one sibling-deployment reachability sheet per verifier family: which contracts, chains, entrypoints, and token balances all trust the same exported-key lineage today?
- When one deployment is rescued or exploited, assume the next copy is already in play. Sweep for identical verifier constants, copied
zkeyartifacts, and matchingverification_key.jsonoutputs before treating the first incident as contained.
Failure-prevention checklist
- Treat Groth16 setup completion as a funded-deploy gate for every chain, not a one-time repo assertion. Base and Ethereum should each have carried an independently reviewable proof that their deployed verifier constants came from finalized artifacts rather than from the same copied pre-contribution family.
- Keep one machine-readable verifier-family registry that groups sibling deployments by exported-key lineage or constant fingerprint, so
gamma == delta == G2 generatorimmediately resolves to every livecollect()surface and reachable reward-token balance in scope. - Refuse claim-system rollout when verifier success is direct payout authority and the verifier-family lineage is still ambiguous. A lottery-style
collect()path with caller-chosen_rewardbits, recipient, fee, and refund fields should not have been allowed to guard live balances until the exact finalized key was rederived and matched onchain. - Add property tests against the live claim surface, not just the circuit: pick accepted roots, fresh nullifiers, maximal reward bits, and attacker-controlled recipients, then prove no public-key-only construction can make
withdraw.verifyProof()pass. - Keep family-level emergency sheets prebuilt for copied verifier deployments: chain, verifier address, claiming contract, funded token, reachable balance, pause or rescue owner, and rescue order should already exist before a malformed-key report turns public.
- Treat copied verifier artifacts as shared blast radius until proven otherwise. Once one deployment fingerprints as a zero-contribution / placeholder family, every sibling deployment should move to
quarantined until distinct lineage is proven, notprobably okay unless separately exploited. - Bound balances behind new proof-authorized claim systems until verifier constants survive independent artifact review and a sibling-deployment sweep confirms no copied malformed family remains funded elsewhere.
- Require release tooling to preserve the exact ceremony/export commands and artifact hashes used to generate the deployed verifier, so responders can answer
which funded contracts share this bad key family?without reconstructing lineage during the incident.
Audit prompts
- What exact finalized artifact and hash produced the Base verifier, what exact finalized artifact and hash produced the Ethereum verifier, and what evidence shows neither came from a pre-contribution
zkeyor copied placeholder verifier JSON? - If
collect()were given a live root, a fresh nullifier,_rewardbits = 7, attacker-chosen recipient data, and zeroed relayer / fee / refund fields, what exact proof-system property still makes payout impossible without a real winning witness? - Which contracts on every chain trust this verifier family today, what token balances or claim rights remain reachable through them, and where is that grouped blast-radius view maintained?
- Do deployment scripts or CI explicitly reject
gamma == delta, generator-valued independent parameters, or other known malformed-key fingerprints before contracts can be funded? - If one verifier in this family is found malformed after launch, who can pause, rescue, or drain each sibling deployment first, and what value-ordering decides rescue priority?
- Are teams reviewing the economic consumer together with the cryptographic artifact? In other words, who signed off that
verifyProof()on this exactcollect()surface was safe to treat as direct reward-transfer authority? - What evidence proves the deployed verifier constants match the finalized export exactly, rather than merely coming from the same repository or generation script in principle?
- If a reviewer searched for identical constant tuples or exported-key hashes across sibling repos and chains today, would that search return every Foom deployment that shares this artifact family without manual archaeology?
Lessons that should already be standard
- A verifier-family fingerprint is already an incident object. Once one funded deployment shows
gamma == delta == G2 generator, the burden shifts to proving which siblings are distinct and safe, not to waiting for each chain to get its own exploit transaction. - Fingerprint the full deployed verifier family, not just the memorable symptom.
Same malformed constant tuple / exported-key lineageis the operational grouping key responders need if they want one rescue-order sheet instead of many loosely related incident tabs. - Proof-authorized payout apps are still custody systems. A lottery
collect()path with fancy zk framing is, operationally, a hot withdrawal rail the moment verifier success directly moves reward tokens. - Cross-chain reuse multiplies zk release mistakes faster than postmortems do. Copying one malformed verifier family into Base and Ethereum turned one setup omission into one shared rescue race.
The circuit is fineis irrelevant when the deployed key is not. Standard Groth16 verifier code plus an unsound exported key is just a standardized asset-release bug.- Family-level quarantine needs to be operational, not rhetorical. The useful response object is one reachability sheet covering sibling verifiers, claiming entrypoints, and funded balances, not separate chain narratives written after responders already noticed both copies.
- Ceremony hygiene and artifact lineage are part of the live payout boundary. If a team cannot quickly prove where a verifier came from, that uncertainty already belongs in the solvency and incident-response model.
Why this incident matters
- It broadens the new proofs/verifiers corpus beyond Aztec-style statement and consumer-binding failures into verifier-key setup failure.
- It is a strong near-miss exemplar because it shows the same verifier-key flaw playing out on a different application surface and on two chains at once.
- It preserves the sharper fleet-level lesson that artifact lineage is itself part of blast-radius management: once one funded verifier fingerprints as
gamma == delta == G2 generator, sibling deployments sharing that verifier family should be treated as one compromised surface until proven otherwise. - It sharpens the newer positive-design lesson from verification-key-statement-inventory-and-asset-reachability-discipline: the missing control was not only
know where this verifier came from, butgroup every deployment carrying the same quarantined artifact-family fingerprint into one reachability and response object before attackers choose which chain to hit first. - It preserves an operational lesson that is easy to lose in zk discussions: ceremony and release artifacts are part of the live custody boundary, not a pre-production detail.
Internal linkages
- Core recurring pattern: verification-key-independence-collapse.
- Live-verifier inventory companion: verification-key-statement-inventory-and-asset-reachability-discipline — the sharper positive-design question is how one quarantined verifier-artifact fingerprint should have pulled Base and Ethereum into one shared blast radius with one reachable-balance map, one quarantine decision, and one rescue order.
- Adjacent release-integrity companion: exact-production-artifact-attestation-and-rollback-discipline — useful when the sharper question is how a zero-contribution / placeholder verifier family should have been stopped from graduating into two funded deployments at all.
- Companion realized exploit: [[veil-cash-groth16-delta-gamma-setup-collapse-forged-withdrawals-2026-02-21|Veil Cash Groth16
delta == gammasetup collapse / forged withdrawals (2026-02-21)]]. - Adjacent proof-statement contrast: witness-public-input-binding-failure.
- Adjacent proof-consumer contrast: proof-to-effects-binding-failure.
Control surface
-
The leverage sat in the deployed Groth16 verification keys, the verifier
IC[]constants used to formvk_x, the artifact lineage that copied that malformed key family across chains, and Foom’scollect()path that turned caller-supplied_rewardbits, recipient data, and verifier success into immediate reward-token transfer authority. -
The decisive security question was not
did the claim rules look right?butdid the onchain verifier key still represent a sound proof system at all, and would one artifact-family fingerprint automatically collapse every sibling deployment into the same blast-radius and reachable-balance response object? -
Sources:
- https://github.com/DK27ss/FoomClub-1.6M-PoC/blob/main/README.md
- https://blog.zksecurity.xyz/posts/groth16-setup-exploit/
- https://app.blocksec.com/phalcon/explorer/tx/base/0xa88317a105155b464118431ce1073d272d8b43e87aba528a24b62075e48d929d
- https://app.blocksec.com/phalcon/explorer/tx/eth/0xce20448233f5ea6b6d7209cc40b4dc27b65e07728f2cbbfeb29fc0814e275e48
-
Last reviewed: 2026-07-31 UTC