Veil Cash Groth16 delta == gamma setup collapse / forged withdrawals (2026-02-21)
- Name: Veil Cash Groth16
delta == gammasetup collapse / forged withdrawals - Date: 2026-02-21
- Protocol: Veil Cash
- Chain: Base
- Category: verification-key independence collapse / unfinalized Groth16 setup / forged proof withdrawals
- Loss:
2.9 ETHdrained from the pool, roughly$5kat the time - Summary: Veil Cash deployed a Tornado-style privacy pool on Base behind a Groth16 verifier whose verification key never preserved the independence assumptions the proof system needed. The deployed verifier’s
gamma2anddelta2values were identical and equal to the BN254G2generator, which is the exact placeholder state left behind by an unfinished or improperly finalized setup. That made proof verification collapse into a tautology: the attacker could chooseA = alpha,B = beta, computevk_xfrom attacker-chosen public inputs, setC = -vk_x, and makeverifyProof()returntruewithout knowing any deposit secret or valid witness. In one transaction, the attacker looped29times, used nullifiers0xdead0000through0xdead001c, keptrelayer,fee, andrefundat zero so thoseIC[]terms vanished, and withdrew the pool’s full2.9 ETHbalance in0.1 ETHchunks. - Trigger / entry point:
- The exploit entry point was Veil Cash’s Tornado-style
withdraw()path on privacy pool0xD3560eF60Dd06E27b699372c3da1b741c80B7D90on Base. - That pool trusted Groth16 verifier
0x1E65C075989189E607ddaFA30fa1a0001c376cfdas direct authority for withdrawals. - The verifier key embedded in that contract had
gamma2 == delta2, so proof success no longer meantthe caller knows a valid deposit witness.
- The exploit entry point was Veil Cash’s Tornado-style
- Root cause:
- The Solidity verifier logic was not the core problem; the deployed verification key itself was unsound.
- Veil’s verification key left
gamma2anddelta2equal to the BN254G2generator instead of distinct ceremony-derived points. - Public analysis tied that shape to the canonical
snarkjsGroth16 Phase-2 omission failure mode: exporting verifier material from the initial pre-contributionzkeystate leavesgamma2anddelta2parked on the same placeholder generator point. - In Groth16, those parameters are supposed to be independent. Once they collapse to the same public point, an attacker can algebraically cancel the public-input term with a forged
C = -vk_xand cancel thealpha/betaterm withA = alpha,B = beta. - The application then honored verifier success as full withdrawal authority, so cryptographic setup failure became direct reserve loss.
- Exploit path:
- The attacker targeted Veil Cash’s
0.1 ETHprivacy pool on Base and inspected the deployed verifier constants. - They observed that
gamma2anddelta2were identical and equal to the BN254G2generator, meaning the verifier key had been deployed in a degenerate state. - In transaction
0x5ff6dbc33e77fab8dc086bb9ea3c88f1ba81df198d24ec9fc0c5b50fb1a4a17dat Base block42,410,815, the attacker used contract0x5F68aD46F500949FA7E94971441F279A85cB3354to automate the drain. - The attack contract read the pool’s latest known Merkle root and denomination, chose recipient
0x49A7CA88094B59b15EaA28C8c6d9BFAb78d5F903, and iterated through fake nullifiers0xdead0000through0xdead001c. - For each forged withdrawal, it kept
relayer = address(0),fee = 0, andrefund = 0, so the correspondingIC[]terms vanished, then computedvk_xfrom the remaining public inputs and the verifier’s hardcodedIC[]points. - It then set proof elements to the public-key-derived forgery:
A = alpha1,B = beta2, andC = -vk_x. - Because
delta2 == gamma2, the pairing equation reduced to a tautology, soverifyProof()returnedtrueeven though no valid witness existed. - The pool paid
0.1 ETHto the attacker-controlled recipient each time. - Repeating that flow
29times drained the full2.9 ETHpool balance in a single transaction, leaving the pool at0and the attacker recipient up2.9 ETHin the reproduced PoC path.
- The attacker targeted Veil Cash’s
- What the attacker needed to be true:
- The deployed verifier key had to preserve the exact
gamma2 == delta2degeneracy that destroys Groth16 soundness. - The pool had to trust verifier success directly for withdrawal authorization.
- The contract had to accept attacker-chosen public inputs as long as they met superficial checks like
isKnownRoot(root)andnullifierfreshness. - The attacker needed to compute
vk_xfrom public inputs and the onchainIC[]constants, which the verifier made fully public. - The pool needed enough balance to make repeated forged withdrawals worthwhile.
- The deployed verifier key had to preserve the exact
- Technical anatomy worth remembering:
- This was not a subtle circuit bug. It was a verifier-key failure: the proof system’s root setup assumptions were already broken before witness logic mattered.
- The exploit is memorable because the forged proof used only public verification-key material:
A = alpha,B = beta,C = -vk_x. - The public-input shape matters: with
relayer,fee, andrefundall zero, the attacker only had to make the live root, fake nullifier, and recipient terms line up insidevk_x. - The fake nullifiers were intentionally obvious (
0xdead0000,0xdead0001, …0xdead001c), which makes the incident a crisp example ofproof verifiedno longer meaningreal private state existed. - Veil’s use of a Tornado-style fixed-denomination pool meant every successful forged proof immediately became real ETH payout authority.
- The published PoC reproduced the end state with pool balance
2.9 ETH -> 0, attacker balance0 -> 2.9 ETH, and about8,314,445gas used across the draining transaction path.
Three proof-authority promotions this incident collapsed
-
A verifier artifact existed at all
- Veil had a deployed Groth16 verifier contract with hardcoded
alpha,beta,gamma2,delta2, andIC[]points, so at a glance the pool looked like it had a normal proof gate in front of withdrawals. - That first fact only established
there is a verifier-shaped object onchain, notthe object came from a finalized safe ceremony.
- Veil had a deployed Groth16 verifier contract with hardcoded
-
That artifact deserved to count as canonical withdrawal-proof truth for this exact pool
- Veil silently promoted the malformed verifier into statement truth even though
gamma2 == delta2 == G2 generatormeant the verification key never preserved Groth16’s independence assumptions. - Once that second promotion failed,
verifyProof()no longer meantthe caller knows a valid Veil deposit witness for this root / nullifier / recipient tuple; it only meantthe attacker found public inputs and public key material that satisfy a broken pairing equation.
- Veil silently promoted the malformed verifier into statement truth even though
-
That accepted proof truth deserved full ETH release authority
- The pool then gave the accepted verifier result direct fixed-denomination withdrawal power with no stronger backing or provenance boundary behind it.
- That is why a setup-artifact mistake turned immediately into reserve loss: a malformed proof gate was still allowed to cash out
0.1 ETHrepeatedly until the entire2.9 ETHpool was empty.
- Representative transactions and addresses:
- Exploit tx:
0x5ff6dbc33e77fab8dc086bb9ea3c88f1ba81df198d24ec9fc0c5b50fb1a4a17d - Exploit block: Base
42,410,815 - Vulnerable pool:
0xD3560eF60Dd06E27b699372c3da1b741c80B7D90 - Vulnerable verifier:
0x1E65C075989189E607ddaFA30fa1a0001c376cfd - Attacker contract:
0x5F68aD46F500949FA7E94971441F279A85cB3354 - Attacker recipient:
0x49A7CA88094B59b15EaA28C8c6d9BFAb78d5F903
- Exploit tx:
- Response / mitigation:
- Public reporting centered on the broken verifier key and released a working proof-of-concept showing the full drain.
- No recovery or whitehat rescue was reported for the
2.9 ETHalready withdrawn. - The durable lesson was not
patch the circuitbutrebuild and redeploy from a completed trusted-setup pipeline, then verify the deployed key matches finalized artifacts. - In
snarkjsterms, that means not exporting or deploying verifier constants from a zero-contribution*_0000.zkeyartifact and proving that a realzkey contribute/ finalization step happened before release. - zkSecurity’s follow-up matters operationally too: after understanding the exploit, they asked Dedaub to scan EVM chains for contracts that stored the same
G2element twice, using the exactgamma == deltafingerprint as a fleet-level detection heuristic rather than treating Veil as a one-off privacy-pool bug. - The healthier response object was not
review this one compromised pool, butopen one family-level quarantine sheet for every verifier, contract, chain, and reachable balance carrying the same malformed-key fingerprint, then work that sheet until each sibling deployment is rescued, drained, or proven clean. - Their reported result was that no other relevant live
gamma == deltaGroth16 verifiers with significant recent activity or value were found in that broader scan, but the response pattern is the lasting lesson — once one funded verifier shows a quarantined artifact-family fingerprint, sibling deployments and adjacent repos should be swept immediately.
Pattern tags
- verification-key-independence-collapse
- verification-key-statement-inventory-and-asset-reachability-discipline
- trusted-setup-finalization-and-verifier-family-quarantine
- exact-production-artifact-attestation-and-rollback-discipline
- Live-verifier reachability and sibling-deployment quarantine should have started the moment
gamma == delta == G2 generatorwas recognized on a funded contract.
Why the exploit worked
- Veil treated
verifyProof() == trueas direct ETH-withdrawal authority, so once the verifier key was unsound there was no second custody or provenance boundary left to stop payout. - The malformed key let the attacker satisfy the verifier with public key material alone:
A = alpha,B = beta, andC = -vk_xfor attacker-chosen public inputs. - Veil’s public-input checks were still easy to satisfy with fake-but-acceptable values: a known live root, fresh nullifiers, a recipient the attacker controlled, and zeroed relayer / fee / refund fields that simplified
vk_xconstruction. - Release engineering and runtime inventory both failed closed in the same direction: the protocol funded a verifier family that still carried the recognizable zero-contribution
gamma == deltafingerprint, and no family-level quarantine sweep started until after loss.
Detection and audit pivots
- Re-audit the deployed verifier constants themselves, not only the circuit or generated Solidity template: for Groth16, explicitly compare
gamma2,delta2, and known placeholderG2generator values onchain. - Try the public-key-only forgery against the exact live contract surface: can a reviewer choose arbitrary accepted public inputs, compute
vk_xfrom onchainIC[], and makeverifyProof()pass without any witness? - Inventory every funded contract and residual exit path that still trusts the same verifier or exported-key lineage, then group them into one artifact-family response object instead of reviewing one pool at a time.
- Treat one malformed-key fingerprint as a hunting rule, not a postmortem detail: if one verifier stores the same
G2element twice, sweep sibling deployments, repos, CI artifacts, and copied verifier constants immediately.
Failure-prevention checklist
- Treat Groth16 setup completion as a hard deploy gate: a funded privacy pool should not launch unless the team can show the finalized
zkey, the exported verifier key, and the exact deployed constants all reconcile automatically. - Refuse deployments where supposedly independent ceremony outputs are equal, generator-valued, or match known zero-contribution artifact fingerprints like
gamma == delta == G2 generator. - Add deployment-time and post-deployment sanity checks that read the live verifier constants onchain and compare them against the finalized ceremony output instead of trusting local build directories or copied
verification_key.jsonfiles. - Run adversarial property tests against the exact withdrawal surface, not only the circuit: choose accepted roots, fresh nullifiers, attacker-controlled recipients, and zeroed relayer / fee / refund fields, then prove that no public-key-only construction can satisfy
verifyProof(). - Keep a machine-readable verifier-family inventory so one malformed-key fingerprint immediately resolves to every sibling verifier, pool, chain, and reachable balance that shares the same artifact lineage.
- Bound balances behind new proof-authorized withdrawal systems until the deployed verifier survives independent artifact review and the family sweep proves no copied malformed verifier remains live elsewhere.
- Preserve pause, rescue, or migration rails while a new verifier family is still fresh in production; the pool should not rely on
proof verifiedas the only remaining custody boundary during rollout. - Require release tooling to preserve the exact ceremony and export commands or hashes used to generate the verifier so responders can prove provenance instead of reconstructing it mid-incident.
Audit prompts
- What exact finalized ceremony artifact produced this verifier, and where can a reviewer independently recompute the exported key hash and deployed constants?
- Do any supposedly independent verification-key elements equal each other, equal known placeholder generator values, or otherwise look implausibly structured for a completed ceremony?
- If a reviewer chooses a live root, a fresh nullifier, an attacker-controlled recipient, and zeroed relayer / fee / refund fields, what exact proof-system property still makes withdrawal impossible without a real note witness?
- Which contracts, chains, or residual exit paths still trust this verifier family today, and where is that grouped blast-radius view maintained?
- Do CI and deployment scripts explicitly reject zero-contribution
zkeyartifacts, copied placeholder verifier JSON, or other known malformed-key fingerprints before contracts can be funded? - What evidence shows the deployed verifier constants match the finalized export exactly, rather than merely coming from the same repo or generation script in principle?
- If one verifier in this artifact family fingerprints as malformed after launch, who can pause, rescue, or drain each sibling deployment first, and what reachable balances are in priority order?
- Are reviewers signing off only on circuit source and Solidity templates, or on the exact onchain verifier constants that actually hold withdrawal authority over live reserves?
Lessons that should already be standard
- The verification key is custody-critical release material, not just build output. Auditing circuit source while hand-waving deployed verifier constants leaves the actual withdrawal authority unreviewed.
- A standard Groth16 verifier template is not a defense. If the constants come from a half-finished setup, the application has deployed a standardized reserve-drain primitive.
- Privacy pools are still custody systems. The moment
verifyProof()directly authorizes ETH release, ceremony hygiene and artifact lineage become part of the live withdrawal boundary. - One recognizable malformed-key fingerprint is already a fleet incident, not a local bug. The useful response object is one grouped sweep over sibling verifiers, copied repos, and reachable balances, not one pool-specific postmortem.
- Artifact provenance should be easier to verify than to narrate. If the team cannot quickly prove where the verifier key came from, that uncertainty already belongs in the solvency and incident-response model.
- New proof-authorized payout systems need bounded rollout just like new bridges or custody code. Until the deployed verifier artifact survives independent scrutiny, balances should stay capped and rescue rails should stay alive.
Why this incident matters
- It is a very clean live exploit where a zk application’s core failure was neither circuit underconstraint nor settlement mismatch, but a malformed deployed verification key.
- The incident makes the release-engineering lesson painfully concrete: a standard verifier template is worthless if the key constants came from a half-finished setup.
- It also shows how directly proof-authorized privacy systems can fail catastrophically once ceremony hygiene becomes part of the custody boundary.
- Veil is also useful as the ecosystem canary for this verifier-artifact family: the same
gamma == delta == G2 generatorfingerprint later surfaced in Foom Cash, and zkSecurity’s own response after Veil was to turn the fingerprint into a chain-wide hunting rule rather than stop at postmortem narrative. - That makes Veil the clean
first funded fingerprintreminder for verification-key statement inventory and asset-reachability discipline: the right next move was one family-level reachability sheet keyed by the malformed verifier artifact, not a one-contract incident file plus vague hopes that no sibling deployment reused it.
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 teams should have known this exact verifier still controlled funded withdrawals, what artifact lineage produced it, how much value one malformed key could reach, and why one bad fingerprint should have opened one sibling-deployment quarantine sheet instead of a contract-by-contract search.
- Companion same-fingerprint near miss: [[foom-cash-groth16-delta-gamma-setup-collapse-whitehat-rescue-2026-02-26|Foom Cash Groth16
delta == gammasetup collapse / whitehat rescue (2026-02-26)]] — useful when the sharper question is how one recognizable verifier-artifact fingerprint should escalate fromincident-specific bugintocross-deployment blast-radius object. - Adjacent proof-statement contrast: witness-public-input-binding-failure — there the verifier key may be sound but the circuit proves the wrong statement; here the verifier key itself destroyed soundness.
- Adjacent proof-consumer contrast: proof-to-effects-binding-failure — there the proof can be sound while settlement drifts; here the verifier would accept forged proofs before settlement logic even became the issue.
Control surface
-
The leverage sat in the deployed Groth16 verification key, the verifier’s hardcoded
IC[]points, and the withdrawal function that treated verifier success as full payout authority. -
The decisive security question was not
did the circuit compile?butdid the onchain verifier constants actually come from a finalized, sound setup, and did operators already have one verifier-family inventory view that would automatically escalate a malformed-key fingerprint into one family-wide reachability and quarantine sweep? -
Sources:
-
Last reviewed: 2026-08-01 UTC