Zero-Knowledge Proof Vulnerabilities — A Technical Classification of Soundness, Completeness, and Protocol Failures
Zero-knowledge proof (ZKP) systems allow a prover to convince a verifier that a statement is true without revealing anything beyond the validity of that statement. As ZK technology has moved from academic constructions into deployed blockchain infrastructure, the attack surface has grown with it. This article classifies the vulnerability categories that have been discovered or exploited in production ZKP systems, from trusted setup misconfigurations to circuit-level soundness failures and protocol boundary gaps, and pairs each category with the incidents that demonstrate it.
This article has been made with the help of Claude Code and several custom skills.
Background — The Three Guarantees a ZK System Must Provide
Any ZK proof system must satisfy three properties.
Completeness. If the statement is true and both parties are honest, the verifier accepts the proof with overwhelming probability. A completeness failure is a denial of service: honest users cannot transact.
Soundness. If the statement is false, no computationally bounded prover can convince the verifier to accept, except with negligible probability. Soundness errors are the dominant source of exploitable bugs.
Zero-Knowledge. The proof reveals nothing about the witness beyond the fact that a valid one exists. Failures here compromise privacy but rarely enable direct fund theft.
The formal soundness condition is:
[\begin{aligned} \Pr\left[\mathcal V(\text{crs}, x, \pi) = 1 \;\middle|\; x \notin \mathcal L,\; \pi \leftarrow \mathcal A(\text{crs}, x)\right] \leq \text{negl}(\lambda) \end{aligned}]
where \(\mathcal L\) is the language of true statements, \(\text{crs}\) is the common reference string, and \(\pi\) is the proof produced by an adversary \(\mathcal A\). Most of the categories below are, in one form or another, a violation of this bound.
Two axes of classification
The same guarantee can break at very different depths of the stack, so the broken-guarantee axis alone does not tell a reviewer where to look. The Salus Security and Ethereum Foundation study Zero-Knowledge Proof Vulnerability Analysis and Security Auditing adds a second axis: the layer at which the defect sits, from the application down to the platform. The two axes together give a review order, since each layer inherits the assumptions of the one below it.

The categories that follow are ordered roughly by depth, starting with the setup that precedes any proof and ending with the ecosystem around the deployed system.
Category 1 — Trusted Setup Misconfiguration
Mechanism
Pairing-based SNARKs such as Groth16 and PLONK require a trusted setup ceremony to generate a common reference string. The Groth16 setup produces structured reference string elements that include two blinding factors, conventionally called gamma (\(\gamma\)) and delta (\(\delta\)), drawn as random scalars and discarded after the ceremony (the “toxic waste”). The resulting verification key publishes \(\gamma G_2\) and \(\delta G_2\) as BN254 G2 points.
The verifier equation checks:
[\begin{aligned} e(A, B) = e(\alpha G_1,\; \beta G_2) \cdot e!\left(\sum_{i=0}^{\ell} a_i L_i,\; \gamma G_2\right) \cdot e(C,\; \delta G_2) \end{aligned}]
where the \(a_i\) are the public inputs and each \(L_i\) is a setup-derived G1 point already divided by \(\gamma\):
[\begin{aligned} L_i = \frac{\beta u_i(x) + \alpha v_i(x) + w_i(x)}{\gamma}\, G_1 \end{aligned}]
The security of the scheme rests on nobody knowing the discrete logarithms \(\gamma\) and \(\delta\). If \(\gamma = \delta = 1\), then \(\gamma G_2\) and \(\delta G_2\) both collapse to the plain G2 generator, the division by \(\gamma\) in \(L_i\) disappears, and the pairing equation loses the algebraic binding that ties \(C\) to a witness. Anyone can then solve for group elements satisfying the remaining check.

Real-world exploitation
Veil Cash (February 20, 2026, ~$5$K). The Groth16 verifier was deployed with gamma and delta both left at the default BN254 G2 generator. The npx snarkjs zkey contribute ceremony step had never been executed and the placeholder constants shipped as-is. An attacker submitted 29 fabricated proofs with nullifier hashes 0xdead0000 through 0xdead001c in a single transaction and drained the contract. Rekt News described it as the first confirmed live exploit of ZK cryptography. Most of the funds were recovered, 2.025 ETH by a whitehat and the remainder returned by the original exploiter.
FoomCash (February 26, 2026, $2.26$M gross). Six days after the Veil Cash post-mortem was published, FoomCash was found to carry the same broken verifier on Ethereum and Base. Two actors drained both pools on the same morning: one took 99.97% of the Base pool over ten loop iterations, retaining roughly $320$K under the protocol’s own “code is law” bounty, while Decurity drained 99.99% of the Ethereum pool and returned $1.84$M for a fee. Net loss was approximately $420$K.
Adjacent case — trusted setup leak
The same layer carries a second, quieter failure: a ceremony that runs but whose participants retain their secret contribution. A multi-party ceremony is secure as long as at least one participant destroys their entropy, so the risk grows as the participant set shrinks or becomes collusive. Concerns of this kind were raised about the original Zcash parameter generation, with no reported exploitation. Veil Cash and FoomCash are the degenerate case of the same problem, where the trapdoor is not merely retained but published in the verification key.
Mitigation
Verification keys must be checked against a reproducible transcript from the ceremony, for example with snarkjs zkey verify. Before deployment the live verifier contract should be tested by submitting a deliberately fabricated proof: if it is accepted, the setup is broken. Proof systems with no trusted setup, such as STARKs and Halo 2, remove this category entirely.
Category 2 — Fiat-Shamir Transcript Binding Failures
Mechanism
Non-interactive proof systems are built from interactive ones through the Fiat-Shamir heuristic: the verifier’s random challenges are replaced by the output of a hash function applied to the transcript so far. The transformation is only sound if the hash covers every prover-controlled value that influences a later verification equation, and covers it before the corresponding challenge is derived.
The “Frozen Heart” vulnerability, named by Trail of Bits in 2022, is what happens when a component is omitted from the hash input. The attacker observes the challenge and then works backwards, choosing the omitted commitment so that the verification equation holds for a false statement.

Trail of Bits found the same class in SnarkJS (Iden3), gnark (ConsenSys) and Dusk Network’s PLONK implementation, and attributed the recurrence to protocol papers that describe the transformation abstractly without specifying the transcript in implementable detail.
Real-world examples
Solana ZK ElGamal, bug #1 (April 16–18, 2025). The ZK ElGamal Proof program, which verifies confidential transfer proofs for Token-2022, omitted algebraic components from its Fiat-Shamir transcript. Forged proofs would have permitted unlimited token minting or unauthorised withdrawal from any confidential token account. Reported by LonelySloth, patched across Agave, Jito-Solana and Firedancer, with validator supermajority adoption inside 26 hours and no loss.
Solana ZK ElGamal, bug #2 (June 10, 2025). Roughly 60 days after the first patch, suneal_eth (@zksecurityXYZ) reported a distinct binding failure in the same program, involving a different excluded component. Token-2022 confidential transfers were disabled the next day and the ZK ElGamal program itself was disabled at epoch 805 on June 19, 2025. Impact was limited because real usage of confidential transfers was near zero: PYUSD, AUSD and USDG had enabled the Token-2022 extension without activating confidential transfers for end users.
Six zkVM Fiat-Shamir failures (September 2025–March 2026, OtterSec). Himanshu Sheoran and Valter Wik disclosed independent binding failures in six zkVM implementations over six months. Each fix was one or two lines of code, and each bug allowed valid proofs of mathematically impossible claims.
| System | Reported | Patched | Fix |
|---|---|---|---|
| Jolt (a16z) | September 2025 | October 3, 2025 | PR #981 |
| Nexus | October 2025 | October 24, 2025 | PR #503 |
| Cairo-M (Kakarot Labs) | October 2025 | October 31, 2025 | commit 92b6740 |
| Binius64 | December 2025 | December 29, 2025 | PR #1355 |
| Expander (Polyhedra) | November 2025 | January 21, 2026 | ~3 month delay |
| Ceno (Scroll) | November 2025 | March 5, 2026 | PR #1262 |
Two disclosure clusters four years apart, spanning roughly a dozen unrelated codebases, make transcript binding a defect to test for by default rather than one to treat as unlikely.
Mitigation
Three controls apply:
- Domain separation. Include a protocol identifier and version in the transcript so that a proof for one circuit or one version cannot be replayed against another.
- Hash everything first. Append all group elements and scalar commitments before squeezing any challenge. This is the distinction Bernhard, Pereira and Warinschi drew between the weak and the strong Fiat-Shamir transform: the weak variant hashes only the commitment, the strong one hashes the statement alongside it, and only the strong one is sound in general.
- Use a transcript abstraction. Merlin, STROBE or a Poseidon-based transcript makes omission structurally awkward, because absorbing and squeezing are separate API calls on one object rather than an ad-hoc hash call site.
After fixing one binding error, re-derive the full list of prover-controlled values and confirm each one is absorbed before the challenge that depends on it. The two Solana bugs show that a point fix says nothing about the rest of the transcript.
Category 3 — Under-Constrained Witnesses
Mechanism
In arithmetic circuit proving systems (R1CS, PLONKish, Halo 2, AIR), circuit code performs two logically separate jobs:
- Witness assignment: computing values for circuit variables from the private inputs, used only to produce the proof.
- Constraint enforcement: emitting the polynomial equations the verifier will actually check.
A relation such as \(a \cdot b = c\) constrains nothing until it is added to the constraint system. In Rust frameworks such as Bellman and franklin-crypto, AllocatedNum::alloc(cs, || Ok(v)) creates a witness variable and adds no constraint at all. In Circom the operator <-- assigns a signal without constraining it, while === and <== emit the constraint. Confusing the two leaves the verifier accepting any value for that variable.
[\begin{aligned}
\text{under-constrained:} \quad &\exists\, a’ \neq a \;:\; \mathcal C(a’, b, c, \ldots) = 0
\text{correct:} \quad &\forall\, a’ \neq a \;:\; \mathcal C(a’, b, c, \ldots) \neq 0
\end{aligned}]
where \(\mathcal C\) is the constraint system evaluated on a candidate witness.
The sub-classes
The family is broader than the headline name suggests, and a review that only greps for <-- will miss most of it:
- Assigned but not constrained. The direct form: a signal receives a value and no constraint pins it. The
IsZerotemplate in circomlib shows the legitimate use of<--, computing a modular inverse that cannot be expressed as a constraint, then immediately constraining the result without <== -in*inv + 1andin*out === 0. The bug is the same pattern with the follow-up constraints missing, as in the PSE zkEVMSHL/SHRopcode circuits, whereshf0was assigned but never forced to equal the first byte ofshift. - Nondeterministic circuits. More than one valid witness exists for the same public output. In a nullifier scheme such as Tornado Cash, this breaks double-spend prevention, because a second spend can present a different nullifier. The
CoreVerifyPubkeyG1circuit in Circom-Pairing calledBigLessThanto checkpubkey < qbut never constrained its output, so a proof withpubkey >= qverified regardless of what the comparison returned. - Mismatching bit lengths. The circomlib
LessThan(n)template feedsin[0] + (1 << n) - in[1]intoNum2Bits(n+1)without constraining the bit length of its inputs, so it effectively compares only the lownbits. Dark Forest v0.3 called it from aRangeProoftemplate, and inputs exceeding the expected bit length produced proofs that passed the range check. - Unused public inputs optimised out. A public input declared but never used in a constraint is removed by the Circom 2.0 optimiser. Semaphore passed a message hash as a public input; without a constraint tying it to the message, an attacker could substitute a different hash and still hold a valid proof, enabling replay against arbitrary messages.

Real-world examples
zkSync Lite, floating-point mantissa (October 5, 2023, $200$K bounty). zkSync Lite serialises transaction amounts in a packed floating-point format to reduce calldata cost. The parse_with_exponent_le function in franklin-crypto decoded it by allocating the mantissa with AllocatedNum::alloc(cs, || Ok(mantissa_value))?, with no constraint linking that variable to the exponent or the packed input. Any (packed_amount, unpacked_amount) pair could therefore be proven. The stated impact covered unauthorised token minting, transfer amount tampering, token theft and state tree freezing. Reported by LonelySloth and fixed on November 7, 2023 by replacing alloc() with into_allocated_num(), which derives the mantissa from the computation and emits the constraint.
Aztec Connect, TurboPlonk claim proof (September 12, 2023, $450,000$ bounty, ~$5$M TVL at risk). The claim proof circuit proves a user’s entitlement to the output of a DeFi interaction through a proportional allocation:
[\begin{aligned} u_{\text{out}} &= \left\lfloor \frac{T \cdot O_a}{O_b} \right\rfloor, & r &= (T \cdot O_a) \bmod O_b \end{aligned}]
with \(T\) the total input, \(O_a\) and \(O_b\) the bridge outputs, \(u_{\text{out}}\) the user’s share and \(r\) the remainder. Two defects combined. First, limbs[3] in the 252-bit decomposition of \(u_{\text{out}}\) carried a 68-bit range constraint instead of a narrower bound derived from MAX_INPUT_BITS, leaving room for limb combinations above the true maximum. Second, \(r\) carried no range constraint at all. A malicious sequencer could pick any remainder and shift \(u_{\text{out}}\) upward, up to the full total_input, inflating one user’s claim at the expense of the shared pool. Reported by lucash-dev; fixed between September 27 and October 3, 2023 by recomputing limb widths from MAX_INPUT_BITS and adding the constraint remainder < bridge_output_b.
zkSync Era, RAM permutation (September 15, 2023, 50,000 USDC bounty, ~$1.9$B at risk). EraVM handles 256-bit values on a field that holds only 253 bits, so the RAM permutation circuit splits each memory value into a lower value and an upper value_residual. Building a MemoryWriteQuery decomposed the upper 128 bits with a LinearCombination:
let mut lc = LinearCombination::zero();
lc.add_assign_number_with_coeff(&u64_word_2.inner, shifts[0]);
lc.add_assign_number_with_coeff(&u64_word_3.inner, shifts[64]);
lc.add_assign_number_with_coeff(&highest_128.inner, minus_one);
// BUG: neither lc.enforce_zero(cs) nor lc.into_num(cs) was ever called
The intent was lc == 0. Because neither terminator was called, the constraint was never produced and the upper 128 bits of every stored value were free:
256-bit register value written by EraVM
┌────────────────────┬────────────────────┬──────────────────────────┐
│ u64_word_3 (64 b) │ u64_word_2 (64 b) │ lowest_128 (128 bits) │
│ UNCONSTRAINED │ UNCONSTRAINED │ constrained │
└────────────────────┴────────────────────┴──────────────────────────┘
intended: u64_word_2 + u64_word_3 * 2^64 - highest_128 == 0
actual: linear combination built, never enforced
L2EthToken.withdraw() records the withdrawal _amount through misaligned stores that create two MemoryWriteQuery entries. A prover could burn 0x133713371337 (about 0.00002 ETH) and rewrite the high 128 bits of the second query to forge a withdrawal message for 0x152d0000133713371337, roughly 100,000 ETH, which the L1 finalizeEthWithdrawal call would honour. Reported by ChainLight, the first ZK circuit bounty paid for zkSync Era. Exploitation additionally required control of the sequencer and surviving the 21-hour execution delay, a barrier that weakens as the protocol decentralises. The fix was a single call to lc.enforce_zero(cs).
Mitigation
- Prefer constructs that make assignment and constraint a single operation:
<==in Circom,into_allocated_num()andcs.enforcein Bellman-derived frameworks. - Treat every
AllocatedNum::alloc(),cs.alloc(),<--and un-terminatedLinearCombinationas a review obligation, not a code style preference. - Add a constraint count test. Derive the expected count from the R1CS or PLONKish specification and fail the build on a mismatch; a missing
enforce_zeroshows up as a number, not as a code smell. - Constrain the bit length of every input to a comparison or decomposition gadget, and constrain the output of every sub-circuit that is used as a boolean.
Category 4 — Over-Constrained Circuits
Mechanism
The mirror image of Category 3 breaks completeness rather than soundness: a circuit carries constraints that a legitimate witness cannot satisfy, so honest provers cannot produce a proof. This arises from redundant hand-written constraints, and also from compiler behaviour, since Circom and Halo 2 both add assertions during compilation and optimisation.
The consequence is a liveness failure rather than theft, which is why this class rarely appears in loss ledgers even though it is a genuine security property of the system.
Real-world example
Scroll zkEVM, Poseidon hash circuit. Early versions used a variable mpt_only to select the hashing mode. With mpt_only = true there should be one custom line and two field elements to hash; otherwise two custom lines. The circuit enabled the custom-line constraint and returned early:
config.s_custom.enable(region, 1)?;
if self.mpt_only {
return Ok(1);
}
The early return left the second line marked as custom and constrained to zero, even though it was meant to carry hash input. Any hash of a non-zero input on that line then failed to prove.
Mitigation
Property-test the circuit against its own specification with a witness generator: for a representative set of valid inputs, proof generation must succeed. A completeness regression is only visible if valid inputs are exercised, which a soundness-focused test suite never does.
Category 5 — Field Arithmetic Over/Underflow
Mechanism
Circuit arithmetic is modular arithmetic over a scalar field, not integer arithmetic. For Circom and the BN254 curve the order is:
p = 21888242871839275222246405745257275088548364400416034343698204186575808495617
Subtraction below zero and addition above \(p-1\) wrap silently:
[\begin{aligned} (0 - 1) \bmod p = p - 1, \qquad \bigl((p - 1) + 1\bigr) \bmod p = 0 \end{aligned}]
A value intended as a small non-negative amount can therefore be supplied as a field element just below \(p\), which behaves as \(-1\) inside the circuit and as an enormous positive integer once interpreted outside it. Comparison gadgets that assume small operands then produce the wrong answer.
The boundary between the circuit and the smart contract is where this most often bites, because Solidity’s uint256 range is strictly larger than the scalar field.
Real-world example
Semaphore, missing contract range check. Semaphore’s verifier rejects proof inputs at or above the SNARK scalar field:
require(input[i] < snark_scalar_field, "verifier-gte-snark-scalar-field");
Group creation, however, accepted any uint256 as the group ID. A group created with an ID above the field order could never have a member proof accepted, permanently bricking that group. The same asymmetry in the other direction, a contract accepting a value the circuit silently reduces, is a soundness bug rather than a liveness one.
Mitigation
Range-check every value crossing the contract-to-circuit boundary on both sides, and test each circuit with inputs deliberately placed at \(p-1\), \(p\) and \(p+1\).
Category 6 — Trivially Bypassed Proof Verification
Mechanism
The simplest ZK failure is not cryptographic at all: the verification never runs. Typical shapes are a stub return true left in production, an emergency path that receives far less test coverage than the main path, and an upgrade that swaps a working verifier for a broken one.
Real-world examples
ZKSwap, return true in verifyExitProof() (July 9, 2025, $5$M). The bridge’s emergency exit function opened with an unconditional return true, leaving all proof verification as unreachable dead code:
function verifyExitProof(bytes memory proof, uint256[] memory publicInput)
internal returns (bool)
{
return true; // dead-code gate: verification never runs
// unreachable below this point:
uint256[] memory vk = getVerificationKey();
return Verifier.verify(vk, proof, publicInput);
}
The attacker triggered Exodus Mode, submitted fabricated exit proofs across 15 token IDs, credited fake balances into balancesToWithdraw and called the ordinary withdraw() path. The exploit contract was deployed at 14:12:35 UTC and the first withdrawal executed at 14:25:23 UTC. The rollup’s last verified L2 block dated from February 2, 2025, so the emergency path had gone unreviewed for months.
HermesVault (May 19, 2026, $29,466$). An Algorand privacy protocol whose withdrawal verification script contained a flaw in its key-reset defence logic. Resetting the keys let an attacker bypass ZK verification entirely and withdraw without a valid proof. The lead engineer confirmed the core ZK circuit was sound; the defect sat in the auxiliary withdrawal script. The protocol shut down permanently after the incident. It is a useful counterexample to the assumption that auditing the circuit audits the system.
Mitigation
- Integration tests must submit deliberately invalid proofs to every verification path, emergency exits included, and assert rejection.
- Emergency paths should be activated on a fork of mainnet state before any upgrade ships.
- Deployed verifier behaviour should be monitored, not just its source; both ZKSwap and Veil Cash were detectable from a single fabricated-proof probe against the live contract.
Category 7 — Protocol Boundary Failures
Mechanism
A ZK-rollup settles when two components agree on the same data: the proof, which commits to the block through a hash or polynomial commitment, and the L1 settlement contract, which applies the state changes. If a parameter that governs the scope of L1 processing is read from raw calldata instead of from the ZK-verified public inputs, the attacker can set it independently of what the proof commits to. The proof then verifies correctly while attesting to a different block than the one L1 settles.
Real-world example
Aztec Connect settlement bypass (June 14–15, 2026, $2.28$M). RollupProcessorV3.processRollup() read numRealTxs directly from calldata. The circuit committed to all 32 transaction slots through the SHA-256 precompile, but the L1 settlement loop iterated only up to numRealTxs.

Setting numRealTxs = 1 with fabricated deposit rows packed into slots 2 to 32 produced L2 balances that L1 had never funded. The attacker then submitted ordinary withdrawal proofs against those balances. Day 1 drained roughly $2.19$M in a single 4.5M-gas transaction (270,513 DAI, 908.99 ETH, 167.89 wstETH, 4,873.86 yvDAI, 16.57 yvWETH and 9,273.73 LUSD), with a second attacker replicating the technique the next morning for approximately $88$K. SlowMist attributed the root cause to the unconstrained gap between numRealTxs and the decoded slot count. Phalcon noted a compounding defect: processRollup() was documented to require an authorised rollup provider or an open escape hatch, and enforced neither.
Aztec Connect had been deprecated and inactive for three years, with immutable contracts still holding user funds and no team monitoring them.
The liveness counterpart
The same boundary produces failures that stop the rollup instead of draining it. The fAmulet study of Polygon zkRollup defines two kinds of finalisation failure bug: fast finalisation failures, where L2 components cannot batch pooled transactions at all, and hard finalisation failures, where sequenced batches never receive a proof on L1. Fuzzing the finalisation behaviour model uncovered twelve zero-day bugs in Polygon zkRollup, with further findings in Scroll zkRollup and Optimism. Cross-chain bridges that wait on L1 finalisation malfunction when this process halts, so a liveness bug at the boundary still carries financial consequences.
Mitigation
Any parameter that governs how much of a committed block is processed must be a public input to the proof, constrained by the circuit to equal the value the settlement contract reads from calldata. Wherever the two sources are permitted to disagree, an attacker can make them disagree.
Category 8 — Range Proof Validation Failures
Mechanism
Range proofs are a ZK primitive distinct from general-purpose SNARKs. In Monero-derived privacy chains, amounts are hidden in Pedersen commitments and their bounds are proven with Bulletproofs:
[\begin{aligned} C &= v G + r H, & \text{prove } v &\in [0,\; 2^{64}) \end{aligned}]
where \(G\) and \(H\) are independent generators, \(v\) is the amount and \(r\) the blinding factor. The commitment alone is homomorphic and hides \(v\) completely, so nothing but the range proof prevents a negative amount. A value just below the group order behaves as a small negative number in the balance equation and as an enormous positive one when interpreted as a token amount, which mints supply out of nothing.
Real-world example
Haven Protocol (December 10, 2024). Haven Protocol is a Monero fork adding oracle-priced synthetic assets (xUSD, xBTC) backed by XHV. A weakness in range proof validation, introduced with the Haven 3.2 rebase onto Monero, let transactions carrying out-of-range amounts pass consensus and mint XHV undetected. Exchange-reported supply exceeded 500 million XHV against an audited supply of roughly 263 million. The team advised exchanges to halt all XHV pairs. Haven had also suffered mint-path attacks in June 2021, resolved by a chain rollback and hard fork.
Mitigation
Range constraints are exactly the constraints most worth formally verifying, because their failure is silent and their correctness is mechanically checkable. Add consensus-level tests for boundary values at \(0\), \(2^{64}-1\), \(2^{64}\) and just below the group order, and re-run the full range-proof test suite after any rebase onto an upstream codebase.
Category 9 — Non-Standardised Cryptographic Implementation
Three defects share a root cause: a primitive implemented from a paper’s description rather than from a specification precise enough to implement against.
- Bad randomness. Blinding factors, nonces and challenges must come from a CSPRNG. Reused, predictable or arithmetically related blinding factors leak the secret, exactly as in Schnorr or ECDSA nonce reuse. Even a few leaked bits per signature can be enough to recover the key. On the verifier side, a predictable challenge lets an adversary prepare a passing proof in advance.
- Bad polynomial implementation. Polynomial code carries invariants that are easy to violate. In Zendoo’s dense polynomial
add(), the branch taken when the operands have equal degree does not trim trailing zero coefficients, so adding[3, 2, 1]and[1, 0, p-1]yields[4, 2, 0]with a zero leading coefficient. The subsequentdegree()call asserts that the leading coefficient is non-zero, turning a representation violation into a panic and a denial of service. - Deprecated hash functions. MD5, SHA-1, RIPEMD-128 and Whirlpool no longer offer collision resistance. A commitment scheme built on a colliding hash lets an attacker construct a second note matching a stored commitment. Tornado Cash’s
CommitmentHasheruses Pedersen hashes overNum2Bitsdecompositions; substituting a broken hash there would convert a privacy pool into an open withdrawal.
Mitigation
Prefer ZK-friendly hashes with published security analysis (Poseidon, Pedersen, MiMC), source all secret randomness from a CSPRNG, and treat polynomial arithmetic as a library to be property-tested against its algebraic invariants rather than as glue code.
Category 10 — Ecosystem and Supply Chain
Two failure modes sit outside the proof system but inside the threat model of anyone deploying one.
SDK supply chain. ZK protocols concentrate large TVL among comparatively few developers, and their SDKs are installed on machines that hold wallets, keys and API credentials. In the dYdX npm/PyPI attack (January 27–28, 2026) a threat actor compromised a maintainer’s credentials and published malicious versions of @dydxprotocol/v4-client-js (3.4.1, 1.22.1, 1.15.2, 1.0.31) and dydx-v4-client (1.1.5post1). The npm payload sat inside createRegistry() and exfiltrated seed phrases and private keys; the PyPI payload added a remote access trojan through config.py and _bootstrap.py. Socket detected the packages on January 27 and dYdX was notified the following day.
Note. dYdX v4 is not itself a zero-knowledge protocol. It is a standalone Cosmos SDK app-chain secured by CometBFT consensus, with no proof system in its trust model; only the legacy dYdX v3 settled on StarkEx. The incident is included as an illustration of SDK supply-chain risk against a high-TVL protocol, and the technique would apply identically to any client library.
A ZK chain does not make application logic ZK-verified. zkLend (February 11, 2025, $9.57$M) lost funds to a rounding flaw in its lending_accumulator, combined with permitted empty-market deposits and donation-driven accumulator inflation. Inflating the accumulator to 4.069297906051644020 made a deposit of 4.069...021 wstETH yield 2 wei of receipt tokens while permitting a withdrawal of 6.103...029 wstETH, a loop that drained over 2,200 ETH plus USDC, STRK and USDT. Nothing about this is a ZK bug; it happened on StarkNet. A validity proof attests that the state transition followed the program’s rules, and says nothing about whether those rules were the intended ones.
Incident Ledger
| Protocol | Date | Loss or risk | Category | ZK defect |
|---|---|---|---|---|
| Aztec Connect claim proof | Sep 12, 2023 | $0$ (~$5$M TVL, $450,000$ bounty) | Under-constrained witness | Yes |
| zkSync Era RAM permutation | Sep 15, 2023 | $0$ (~$1.9$B at risk, 50K USDC) | Under-constrained witness | Yes |
| zkSync Lite mantissa | Oct 5, 2023 | $0$ ($200$K bounty) | Under-constrained witness | Yes |
| Haven Protocol | Dec 10, 2024 | Unbounded XHV mint | Range proof validation | Yes |
| zkLend | Feb 11, 2025 | $9.57$M | Precision loss on a ZK chain | No |
| Solana ZK ElGamal #1 | Apr 16–18, 2025 | $0$ (patched) | Fiat-Shamir binding | Yes |
| Solana ZK ElGamal #2 | Jun 10, 2025 | $0$ (patched) | Fiat-Shamir binding | Yes |
| ZKSwap | Jul 9, 2025 | $5$M | Verification bypass | Yes |
| ZKSpace | ~Jul 2025 | ~$4$M | Proxy contract exploit | Partial |
| Six zkVMs (OtterSec) | Sep 2025–Mar 2026 | $0$ (disclosed) | Fiat-Shamir binding | Yes |
| dYdX SDK packages | Jan 27–28, 2026 | Undisclosed | Supply chain | No |
| Veil Cash | Feb 20, 2026 | ~$5$K | Trusted setup misconfiguration | Yes |
| FoomCash | Feb 26, 2026 | $2.26$M gross, ~$420$K net | Trusted setup misconfiguration | Yes |
| HermesVault | May 19, 2026 | $29,466$ | Verification bypass (key reset) | Yes |
| Aztec Connect settlement | Jun 14–15, 2026 | $2.28$M | Protocol boundary | Yes |
Two patterns stand out. Every row with a large sum at risk and no loss (zkSync Era at roughly $1.9$B, Aztec Connect’s claim proof, zkSync Lite, both Solana bugs and the six zkVMs) was found by a researcher reading circuit or proof-system code and disclosed responsibly. Separately, the three largest exploited losses caused by a genuine ZK defect (ZKSwap, Aztec Connect, FoomCash) all struck protocols that had been dormant for months or years, Aztec Connect for three.
Defense Strategies
The controls map onto the categories:
| Vulnerability class | Primary control |
|---|---|
| Trusted setup misconfiguration | Reproducible ceremony transcript; probe the live verifier with a fabricated proof before deployment |
| Fiat-Shamir transcript binding | Transcript abstraction with domain separation; absorb every prover value before squeezing any challenge |
| Under-constrained witnesses | Constraint count test against the specification; ban bare alloc(); prefer <== over <-- |
| Over-constrained circuits | Property tests asserting that valid witnesses still produce provable statements |
| Field arithmetic over/underflow | Range checks on both sides of the contract-to-circuit boundary; boundary tests at the field order |
| Verification bypass | Integration tests asserting rejection of invalid proofs on every path, emergency exits included |
| Protocol boundary | Every iteration-bound and scope parameter is a constrained public input |
| Range proof failures | Formal verification of range constraints; consensus tests for boundary values after every rebase |
| Non-standardised cryptography | CSPRNG for all secret randomness; ZK-friendly hashes; property-tested polynomial arithmetic |
| Supply chain | Dependency pinning, 2FA on npm and PyPI, reproducible builds |
Audit methodology
The Salus study recommends a divide-and-conquer review that follows the layer stack shown earlier: threat-model each layer separately, produce a per-layer list of candidate defects, then perform line-by-line inspection with internal cross-review. Three habits make it effective in practice:
- Specify before implementing. Write the circuit in R1CS, PLONKish or AIR form first. An unconstrained variable is visible as a gap between the specification and the emitted constraint system, and invisible in the code.
- Look outward from the circuit. HermesVault’s circuit was sound and its withdrawal script was not; ZKSwap’s verifier was correct and unreachable; Aztec Connect’s proof was valid and its settlement loop ignored it. The circuit is where reviewers look and the integration is where funds are lost.
- Exploit library reuse deliberately. ChainLight found the zkSync Era
LinearCombinationdefect because they had met the same footgun in Matter Labs’ circuit libraries while building their own protocol. A pattern found in one consumer of a shared circuit library is worth grepping for across every other consumer.
Conclusion
Zero-knowledge proof vulnerabilities span a wider surface than the name suggests. The cryptographic core fails through trusted setup shortcuts, Fiat-Shamir transcript omissions, under-constrained witnesses and field arithmetic wraparound. The surrounding infrastructure fails through dead verification code, settlement parameters the proof never sees, range checks that stop at the contract boundary, and compromised distribution channels.
Most of the incidents reviewed here share one trait: the proof was accepted as valid by the verifier while the attacker’s claim was false. Soundness held for the circuit as written, and the circuit did not encode the property that needed proving. Organising the categories by the guarantee that breaks and the layer it breaks on keeps that distinction visible, which a list ordered by incident does not.

Annex
Key Terms
| Term | Definition |
|---|---|
| Soundness | The guarantee that no efficient prover can produce an accepted proof for a false statement, except with negligible probability. Its failure is what enables forged withdrawals and unauthorised minting. |
| Completeness | The guarantee that an honest prover holding a valid witness can always produce a proof the verifier accepts. Its failure is a denial of service, not a theft. |
| Trusted setup | A one-off ceremony producing the common reference string for a pairing-based SNARK. Its secret inputs, the toxic waste, must be destroyed by at least one participant. |
| Toxic waste | The random scalars, notably gamma and delta in Groth16, whose retention or publication lets an attacker forge proofs for arbitrary statements. |
| Fiat-Shamir heuristic | The transformation of an interactive proof into a non-interactive one by deriving each verifier challenge from a hash of the transcript so far. |
| Transcript | The ordered record of prover messages absorbed into the hash before a challenge is squeezed. Any prover-controlled value omitted from it becomes freely choosable after the challenge is known. |
| Witness | The private input that makes a statement true. The prover knows it; the proof convinces the verifier it exists without revealing it. |
| Under-constrained circuit | A circuit in which some variable is assigned a value during witness generation but is not pinned by any constraint, so the verifier accepts alternative values for it. |
| Public input | A value the verifier supplies to the verification equation, and therefore the only channel through which external data is cryptographically bound to the proof. |
| Range proof | A proof that a committed value lies inside a stated interval without revealing it. Bulletproofs over Pedersen commitments are the standard construction in Monero-derived chains. |
Security Implementation Checklist
The items below are properties an implementation either meets or fails. They are derived from the failure modes in the categories above rather than transcribed from a single standard.
Trusted setup and verification keys
| Check | Security requirement | Failure mode if violated |
|---|---|---|
| ☐ | The verification key is reproduced from a published ceremony transcript before deployment (snarkjs zkey verify or equivalent). |
A key carrying placeholder or attacker-known parameters accepts arbitrary proofs, as in Veil Cash and FoomCash. |
| ☐ | Gamma and delta in a Groth16 key are not the G2 generator, and no setup scalar equals 1. | The pairing check degenerates and any fabricated proof verifies. |
| ☐ | The ceremony has at least one participant with a credible claim to having destroyed their entropy. | A retained trapdoor lets that participant forge proofs indefinitely and undetectably. |
| ☐ | The deployed verifier is probed with a deliberately fabricated proof before it holds funds. | A broken setup is indistinguishable from a working one until the first exploit. |
Fiat-Shamir transcript
| Check | Security requirement | Failure mode if violated |
|---|---|---|
| ☐ | Every prover-controlled value that appears in a verification equation is absorbed into the transcript before the challenge that depends on it. | The prover reads the challenge and back-solves the omitted value, forging a proof for a false statement. |
| ☐ | The transcript is domain-separated by protocol identifier and version. | A proof valid for one circuit or version is replayed against another. |
| ☐ | Challenges are derived through a transcript abstraction (Merlin, STROBE, Poseidon sponge) rather than ad-hoc hash calls. | Scattered hash call sites make an omission invisible in review. |
| ☐ | After any binding fix, the full list of prover values is re-derived and each absorption re-verified. | A point fix leaves sibling omissions in place, as in the two Solana ZK ElGamal bugs. |
Circuit constraints
| Check | Security requirement | Failure mode if violated |
|---|---|---|
| ☐ | Every assigned signal or witness variable is pinned by an explicit constraint. | The verifier accepts arbitrary values for it, enabling amount tampering and unauthorised minting. |
| ☐ | Every LinearCombination is terminated by enforce_zero or into_num. |
The constraint is built and never emitted, as in the zkSync Era RAM permutation bug. |
| ☐ | The output of every sub-circuit used as a boolean or comparison result is constrained. | The circuit is nondeterministic: a second witness satisfies the same public output, breaking nullifier and double-spend logic. |
| ☐ | The bit length of every input to a decomposition or comparison gadget is constrained. | Only the low bits are compared, so out-of-range inputs pass the check. |
| ☐ | Every quotient, remainder and limb in a decomposition carries a range constraint derived from the true maximum. | A free remainder shifts the quotient upward, inflating a proportional claim (Aztec Connect claim proof). |
| ☐ | Declared public inputs survive compilation and appear in at least one constraint. | The optimiser removes them, so the value they were meant to bind can be substituted freely. |
| ☐ | The emitted constraint count is compared against the count derived from the specification, in CI. | Missing constraints stay invisible, because the code that should have emitted them still reads correctly. |
| ☐ | Proof generation is tested against valid witnesses, not only invalid ones. | Over-constrained circuits pass a soundness-only suite and block honest users on mainnet. |
Field arithmetic and boundaries
| Check | Security requirement | Failure mode if violated |
|---|---|---|
| ☐ | Every value crossing the contract-to-circuit boundary is range-checked on both sides against the scalar field order. | A uint256 above the field order is silently reduced, or a group is created that no proof can ever satisfy. |
| ☐ | Circuits are tested with inputs at the field order and immediately either side of it. | Wraparound turns a subtraction below zero into a value near \(p\), read outside the circuit as an enormous amount. |
| ☐ | Range proofs bound both ends of the interval, and the bound is re-tested after any rebase onto an upstream codebase. | Out-of-range amounts pass consensus and mint unbacked supply, as in Haven Protocol. |
Verifier integration and protocol boundary
| Check | Security requirement | Failure mode if violated |
|---|---|---|
| ☐ | Every verification path, including emergency exits and escape hatches, has an integration test asserting that an invalid proof is rejected. | A stub return true or an unreachable verifier drains the bridge, as in ZKSwap. |
| ☐ | Every parameter that bounds how much of a committed block is processed is a constrained public input. | L1 processes a different subset than the proof commits to, crediting unbacked balances (Aztec Connect). |
| ☐ | Access control on proof submission matches the documented model and is enforced in code. | Arbitrary callers submit rollup proofs the design assumed only an authorised provider could. |
| ☐ | Auxiliary scripts and key-management paths around the verifier are audited alongside the circuit. | A sound circuit is bypassed through its key-reset logic, as in HermesVault. |
| ☐ | Deprecated or dormant deployments are drained and paused rather than left live. | Immutable contracts keep custody of funds with nobody monitoring them; the three largest exploited losses here were all dormant protocols. |
Cryptographic primitives and supply chain
| Check | Security requirement | Failure mode if violated |
|---|---|---|
| ☐ | All blinding factors, nonces and secret randomness come from a CSPRNG and are never reused or derived from one another. | Predictable or repeated blinding factors leak the witness or the signing key. |
| ☐ | Commitment and Merkle constructions use a collision-resistant, ZK-friendly hash (Poseidon, Pedersen, MiMC), never MD5, SHA-1 or RIPEMD-128. | Collisions let an attacker present a second preimage for a stored commitment and withdraw another user’s funds. |
| ☐ | Polynomial arithmetic maintains its representation invariants, including trimming zero leading coefficients on every branch. | A violated invariant panics in degree() or similar, giving a remote denial of service. |
| ☐ | SDK dependencies are pinned by version and integrity hash, and publishing accounts enforce 2FA. | A compromised maintainer account ships credential-stealing code to every developer who installs the SDK. |
Frequently Asked Questions
Q: What is the difference between a soundness bug and a completeness bug in a ZK circuit?
A soundness bug lets a prover produce an accepted proof for a false statement. This is the class that enables forged withdrawals and unauthorised minting, and it accounts for nearly every incident in this article.
A completeness bug prevents an honest prover from producing a proof for a true statement. The Scroll zkEVM Poseidon circuit over-constrained its second custom line, so any hash of a non-zero input on that line failed to prove.
The practical consequence is asymmetric: soundness bugs cost funds, completeness bugs cost availability. They also need different tests, because a suite built entirely from invalid inputs will never notice that valid ones stopped working.
Q: Why does setting gamma and delta to the G2 generator break Groth16?
The verification equation pairs the proof element \(C\) against \(\delta G_2\) and the public-input combination against \(\gamma G_2\), while the setup-derived points \(L_i\) are already divided by \(\gamma\). The scheme’s security depends on the discrete logarithms \(\gamma\) and \(\delta\) being unknown to everyone.
Setting both to the generator means setting the scalars to 1. The division by \(\gamma\) vanishes, the pairing terms lose their independent blinding, and the equation no longer ties \(C\) to any witness. An attacker does not need to recover the original witness; any group elements satisfying the weakened check are accepted.
Q: How does an unconstrained mantissa in zkSync Lite enable token minting?
zkSync Lite stores transfer amounts in a packed floating-point format and decodes them inside the circuit as unpacked = mantissa * 10^exponent. The decoder allocated the mantissa with AllocatedNum::alloc(), which creates a witness variable and emits no constraint linking it back to the packed input.
The prover therefore controls the mantissa freely. A transaction whose packed amount encodes a small transfer can be proven with a mantissa of, say, 10^15, producing an unpacked amount many orders of magnitude larger. The verifier accepts because nothing in the constraint system ever checks that the packed amount and the mantissa agree.
Q: Why did Solana suffer two independent Fiat-Shamir failures in the same program within 60 days?
Because the two bugs concerned different components of the same transcript, and fixing the first did not trigger a review of the rest. LonelySloth’s April 2025 report identified one omitted algebraic component; suneal_eth’s June 2025 report identified another.
The general lesson is that a Fiat-Shamir fix is not evidence of transcript correctness. A complete review enumerates every prover-controlled value, identifies the challenge that depends on it, and confirms the absorption happens first. Anything short of that leaves sibling omissions in place.
Q: In the June 2026 Aztec Connect exploit the ZK proof was valid. Why was the attack still possible?
The proof committed to all 32 transaction slots through the SHA-256 precompile and correctly attested to the computation over that block. The defect was that RollupProcessorV3.processRollup() did not take its loop bound from the proof’s public inputs; it read numRealTxs straight from calldata.
Setting numRealTxs = 1 and packing fabricated deposit rows into slots 2 to 32 produced a block the circuit hashed in full and L1 settled only one slot of. The deposits were committed by the proof and never funded on L1, so the resulting L2 balances had no backing, and ordinary withdrawal proofs converted them into real assets.
The proof system did its job. The computation it proved was not the computation L1 needed enforced.
Q: Why does “assigned but not constrained” keep recurring across different frameworks and teams?
The root cause is that circuit DSLs deliberately separate witness computation from constraint generation, and the separation is necessary. Some values, such as a modular inverse, cannot be expressed as a constraint and must be computed by assignment first, then constrained afterwards. Circom’s <-- and Bellman’s alloc() exist for exactly that reason.
What makes it dangerous is that the two halves look identical in review. The code computes the right value and stores it, so it reads correctly; the missing half is an absence, and absences do not appear in a diff. Developers arriving from ordinary programming, where an assignment is a binding, have no intuition that the second step is required.
This is why the reliable defence is mechanical rather than editorial: a constraint count compared against the specification turns an invisible absence into a failing number.
Q: A protocol runs on a ZK-rollup and its circuits have been audited. What is still unprotected?
Quite a lot, and the incidents here name each gap:
- The verifier integration. ZKSwap’s proof verification logic was correct and sat behind an unconditional
return true. HermesVault’s circuit was confirmed sound while its withdrawal script’s key-reset logic bypassed verification entirely. - The settlement boundary. Aztec Connect’s proof was valid; the L1 loop bound was not derived from it. Any parameter read from calldata rather than from public inputs is outside the proof’s reach.
- The application’s own arithmetic. zkLend lost $9.57$M to a rounding flaw on StarkNet. A validity proof attests that the state transition followed the program’s rules; it makes no claim about whether those rules were correct.
- The distribution channel. The dYdX SDK compromise stole keys from developer machines without touching any proof system.
A circuit audit answers “does this circuit constrain what it claims to constrain”. It does not answer “is this circuit the right circuit, is it actually invoked, and is its output actually used”.
References
Incident post-mortems and disclosures
- Aztec Connect Rekt — Rekt News
- Analysis of the $2.19$ Million Asset Theft from Aztec Connect — SlowMist
- Default Settings (Veil Cash) — Rekt News
- How ZKSwap’s $5$M Exploit Could Have Been Prevented with Onchain Monitoring — Blockaid
- Malicious dYdX Packages Published to npm and PyPI — Socket
- Post Mortem: ZK ElGamal Proof Program Bug, April 2025 — Solana
- Post Mortem: ZK ElGamal Proof Program Bug, June 2025 — Solana
- SlowMist Hacked database — Haven Protocol (December 10, 2024) and HermesVault (May 19, 2026) entries
- The Unfinished Proof (FoomCash) — Rekt News
- zkLend Rekt — Rekt News
Bug bounty and audit write-ups
- Uncovering a ZK-EVM Soundness Bug in zkSync Era — ChainLight
- zkSync Insufficient Proof Verification Bugfix Review — Immunefi
- zkVMs and Their Unfaithful Claims — OtterSec
- The Frozen Heart Vulnerability in Bulletproofs — Trail of Bits
- Aztec Connect claim proof bug write-up (HackMD) and Aztec multiple-spend bugfix review (Immunefi), September–October 2023
Academic papers
- Zero-Knowledge Proof Vulnerability Analysis and Security Auditing — Tang, Shi, Wang, Charbonnet, Tang, Sun (IACR ePrint 2024/514)
- fAmulet: Finding Finalization Failure Bugs in Polygon zkRollup — Li, Peng, He, Luo, Chen (arXiv 2410.12210)
- How to Prove Yourself: Practical Solutions to Identification and Signature Problems — Fiat, Shamir (CRYPTO 1986)
- How Not to Prove Yourself: Pitfalls of the Fiat-Shamir Heuristic and Applications to Helios — Bernhard, Pereira, Warinschi (ASIACRYPT 2012)
- On the Size of Pairing-Based Non-Interactive Arguments — Groth (EUROCRYPT 2016)
- Bulletproofs: Short Proofs for Confidential Transactions and More — Bünz, Bootle, Boneh, Poelstra, Wuille, Maxwell
Tools
Related articles
- Zero-Knowledge Proof Hacks — A Documented Record of Exploits and Vulnerabilities (2023–2026)
- Zero-Knowledge Proof Failures in Cross-Chain Bridges — Exploits, Vulnerabilities, and Bug Bounties
- How Zero-Knowledge Proofs Get Hacked — Explained for a 10-Year-Old
- Solidity Verifier Contract - Security Checklist