Worked example · 2026-02
Veil Cash — the verifying key set delta equal to gamma, so the pairing equation accepted proofs nobody had to compute — Veil Cash / Base — 2026-02-20
Summary
Veil Cash is a privacy protocol on Base using Groth16 zk-SNARK proofs to let a depositor withdraw to a fresh address without linking the two. Withdrawal is gated by a proof verified on-chain against a verifying key fixed at deployment.
Groth16's soundness depends on the verifying key's gamma and delta elements being independent — they separate the public-input commitment from the proof-specific randomisation. Veil's deployed verifier had both set to the same value: the BN128 G2 generator, which is what a verifier template contains before real ceremony output is substituted in.
With delta2 == gamma2, the pairing check no longer binds a proof to a witness. On 2026-02-20, an attacker deployed a contract that forged proofs for arbitrary public inputs, including nullifier hashes that corresponded to no deposit, and called withdraw() 29 times in one transaction against the 0.1 ETH pool, taking 2.9 ETH. No deposit was ever made.
Decurity identified the attack and moved to rescue the remaining pools. The exploiter later returned the funds. Within days, FoomCash — running the same misconfiguration — was drained by a copycat.
Timeline (UTC)
| When | Event | OAK ref |
|---|---|---|
| (standing) | Verifier deployed with a Groth16 verifying key whose delta2 equals gamma2, both the BN128 G2 generator — the untouched template default |
(latent T9.015.001 defect) |
| (standing) | The Verifier contract is outside the audit engagement's scope; circuit and application review do not cover deployed key material |
(review-scope gap) |
| 2026-02-20 | Attacker deploys a contract that forges proofs for arbitrary public inputs; calls withdraw() 29 times in a single transaction with fabricated nullifier hashes |
T9.015.001 exploitation |
| same transaction | 2.9 ETH — the entire 0.1 ETH pool — leaves to an address that never deposited | (extraction) |
| shortly after | Decurity detects the attack and rescues the remaining pools | (external detection) |
| after | Exploiter returns the drained ETH unprompted | (disposition) |
| days later | FoomCash drained through the same verifying-key misconfiguration | (class repeat) |
What defenders observed
- The proof system did not break; it was deployed broken. There is no bug in Groth16 and none in Veil's circuit. The failure is entirely in the constant that was shipped, which makes it invisible to every review layer that reads code rather than deployed state. Verifying keys are configuration, and configuration is what nobody diffs.
- The check is four lines and anyone can run it.
delta2 != gamma2; neither equals the curve generator; all G1/G2 points are on-curve and in the correct subgroup. Run against the deployed contract's public constants, this class is detectable before an attack, by an outside party, with no privileged access. As far as the public record shows, nobody was running it. - A copycat landed within days, which is how this class propagates. The same default appears wherever a verifier template is deployed without substituting real ceremony output, so one public exploit turns into a scan of every comparable deployment. FoomCash was the second; the population of unaudited-verifier privacy pools is the rest.
- The loss says nothing about the severity. $5K is the pool size, not the blast radius. This is the clearest case in the corpus for separating defect severity from realised loss when scoring: identical defect, identical transaction, and the payout is whatever the contract holds.
- Audit scope excluded the one file that mattered. The auditors said so publicly and precisely. It is the same structural failure as the Exactly Protocol periphery contract (2023-08) and the ether.fi legacy queue (2026-09): scope is drawn around the code the team thinks of as theirs, and attackers read the deployment.
Public references
[rektunfinishedproof2026]— Rekt, "The Unfinished Proof": https://rekt.news/the-unfinished-proof[darknavyveil2026]— DARKNAVY, "Veil Cash Groth16 Forgery" (thedelta2 == gamma2verifying-key misconfiguration and the forged-proof withdrawal path): https://www.darknavy.org/web3/exploits/veil-cash-groth16-forgery/[veilcashpoc2026]— Public proof-of-concept reproduction, "groth16 verifier haddelta2 == gamma2": https://github.com/DK27ss/VeilCash-5K-PoC[pashovveil2026]— Pashov Audit Group statement that the misconfiguredVerifiercontract was out of scope for their Veil engagement: https://x.com/PashovAuditGrp/status/2025598503255167195[coinsbenchveil2026]— CoinsBench, "Forging zkSNARK Proofs via Misconfigured Verification Keys: The Veil_01_ETH Exploit": https://coinsbench.com/forging-zksnark-proofs-via-misconfigured-verification-keys-the-veil-01-eth-exploit-2a6bb7d0078b
Discussion
OAK-T9.015 was minted in 2026-07 on the Bonzo Lend / Supra anchor, where an all-zero signature against a zero-point public key satisfied a pairing equation trivially. The distinction it preserves is between an application-layer check that is missing (T10.002) and a cryptographic primitive that returns true on a degenerate input (T9.015) — different owners, different audit scopes, different fixes. TAXONOMY-GAPS.md proposed T9.015.001 as the sub-class for the same defect in a proof verifier rather than a signature verifier, and flagged it "mint once anchored." This is that anchor, and it arrived from the opposite direction: Bonzo's degenerate input was in the message, Veil's is in the key.
That difference is worth keeping in the definition when the sub-class is minted. A degenerate input is supplied by an attacker at call time and can be rejected by input validation. A degenerate key is supplied by the deploying team once and is validated by nobody — it is trusted precisely because it is a constant. Both produce the same observable (a verifier returning true without a witness), and only the second one is exploitable by everyone from the moment of deployment, permanently, with no precondition.
The corpus now holds two cases in the class (Bonzo 2026-07, Veil 2026-02) plus a same-class repeat at FoomCash days after Veil. Contributors documenting the next one should record which element was degenerate and who supplied it — attacker, ceremony, or template default — because that field determines whether the mitigation is input validation, ceremony verification, or a deployment assertion.