Shout out to Kyle and Gubsheep for creating this resource!

Resources – Source here

Bug 1 and 2 – Under-constrained Circuits + Nondeterministic Circuits

This specific section of vulns is the 80/20 rule. 20% of the vulns impact 80% of the projects – at least today, from this list.

When programming in Circom, Ciaro, and many other ZK domain-specific languages (DSLs) they’re declarative type languages (tell me what), not like Solidity, which is imperative (tell me how).

This means that we’re “declaring” what we want from our program. If we miss one declaration or “constraint”, then we’re possibly opening ourselves up to significant consequences like allowing a user to repeatedly drain funds… That’s what’s happening here.

In order to prevent these bugs, it is important to test the circuit with edge cases and manually review the logic in depth.

Formal verification tools are almost production ready and will be able to catch a lot of common under-constrained bugs, such as ECNE by 0xPARC and Veridise’s circom-coq.


Example 1 –> 0xPARC StealthDrop: Nondeterministic Nullifier (link)

Detailed Twitter thread

@0xB07DAD pointed out a flaw in the current StealthDrop system that allows users to double-claim.

The nullifier is created through an ECDSA signature made of a private key and message.

The ZK circuit enforces the nullifier is valid but doesn’t enforce anything around the creation of the nullifier. ECDSA is nondeterministic in the creation of signatures, so we’re able to manipulate our raw private key and create multiple “valid” nullifiers for the same claim… Which in turn allows us to “claim” multiple drops.

Fix – The ZK circuit needs to enforce the proper creation of the nullifier, ensuring it’s deterministic (same input = same output every time). This can be done by generating a proof on the user’s private key, which is commonly done through a browser-based wallet (read more here)

Example 2 –> MACI 1.0: Under-constrained Circuit (link)


This is a voting dapp that allows users to vote with ZK proofs backing their validity. Each user must submit their public key to the smart contract before voting and they can’t vote without the private key to that pubkey.

Each new vote replaces the current pubkey with a new pubkey, representing that vote.

The coordinator takes the encrypted votes previously published on-chain, takes them off-chain decrypts them, then creates a ZK proof, and resubmits on-chain, this is then validated by a ZK circuit sitting on-chain.

The coordinator is a potential bad actor and the ZK circuit is missing constraints, which is where our exploit sits.

The Bug

Each Pubkey should be bound to a specific “msg”, which is the user’s vote. The circuit is not binding these two, which allows the coordinator to swap pubkeys and msgs, to censor voting (invalidate).

The Fix

Ensure there’s a new constraint in the ZK circuit that binds the pubkeys <–> msgs together.

Bug 3 – Arithmetic Over/Under Flows

Underflow and overflow are well-known within the security community. We’re either going to pop out of our designated range from the top (overflow) or the bottom (underflow)

In the case of circuits, we’re popping out of our range for an interesting reason, which is modular arithmetic. <– Check the link for a simple explanation.

Modular math is used heavily within ZK due to its ability to create strong “trap doors” (i.e. hard to go backward) within the schemes we’re using.

Within Circom there’s a specific “prime field” that we’re using for our modular math. If we go over or under, then we wrap back around, like the clock above.

Example 1 –> Semaphore: Missing Smart Contract Range Check (link)

  • Semaphore – Special club for members only

Example 2 –> Zk-Kit: Missing Smart Contract Range Check (link)

  • ZK toolkit to assist in creating incremental Merkle trees, which is a way to incrementally grow the Merkle tree. This toolkit is used by Semaphore (above) to add new members to the club.

Both examples above run into similar problems.

Background They’re both overlooking a user input that has a direct impact on the ZK proof (i.e. public input). This miss allows the user to maliciously or accidentally overflow the “SNARK scalar field” (i.e. our modular universe) thanks to Solidity’s uint256 available integer values.

Fix The fix simply includes a required statement to ensure the user input is lower than the expected “SNARK scalar field” limit.

Bug 4 – Mismatching Bit Lengths

Many of CircomLib’s circuits take in an expected number of bits (i.e. 32 bits). In order for their constraints and output to be accurate, the input parameters need to be constrained to the maximum number of bits outside of the CircomLib circuit.

Example –> Aztec 2.0: Missing Bit Length Check / Nondeterministic Nullifier (link)

Background Funds are stored in “note commitments” (NC) with Aztec. Every time an NC is spent a nullifier is created alongside that NC from its index position in the Merkle tree. This index position is assumed to be a max size of 32 bits by the circuit creating the nullifier from this variable (but it’s not enforced)

Bug Without a bit range checks on the index variable used to create the nullifier the attacker can continually update the bits outside the 32 “expected” range, spending the same NC over and over again.

Fix Enforce bit range check on the index variable before creating the nullifier.


Bug 5 – Unused Public Inputs Optimized out

Many circuits will have a variable as public input, but won’t write any constraints on that variable. These public inputs without any constraints can act as key information when verifying the proof. However, as of Circom 2.0, the default r1cs compilation uses an optimizer. The optimizer will optimize these public inputs out of the circuit if they are not involved in any constraints.


The function of the optimizer in r1cs

The optimization is skipping the linear “wires” (X^1), which is why the “fix” is to square all the linear “wires” (X^2) and make them non-linear, so they’re not skipped.

More details on optimizing within r1cs

Source: Our automated optimizer allows users to focus on the circuit design and obtain efficient implementations while avoiding error-prone and complex manual optimizations. This way, the construction of circuits can be more transparent and easier to audit.

Linear Skip Optimization: In a nutshell, we leverage the fact that the composition of linear functions is again a linear function in order to “skip” the evaluation of certain wires.

What is the difference between linear and non-linear? video

The Bug

Semaphore is a ZK app that allows users to prove membership in a group and send signals without revealing their identity. In this case, the signal that a user sends is hashed and included as public input to the proof. If the Semaphore devs had not included any constraints on this variable, an attacker could take a valid proof of a user, modify the signal hash (public input) and replay the proof with the modified signal hash. This is essentially forging any signal they like.

The Fix

To prevent this over-optimization, one can add a non-linear constraint that involves the public input (X^2). TornadoCash and Semaphore do this. TornadoCash used multiple non-linear constraints to prevent its public variables from being optimized out. Semaphore’s public “signalHash” input is intentionally added into a non-linear constraint (”signalHashSquared”) to prevent it from being optimized out.

Bug 6 – Frozen Heart: Forging of Zero Knowledge Proofs

Head over to this video for an explanation of this Vuln. Long story short the method used to convert an interactive ZK scheme to a non-interactive scheme can lead to some serious exploits… It comes down to the implementation as always.

Video description here.

Bug 7 – Trusted Setup Leak


Many popular zk protocols require what is known as a trusted setup. The trusted setup is used to generate the parameters necessary for a prover to create sound zk proofs. However, the setup also involves parameters that need to be hidden from everyone. These are known as “toxic waste”. If the toxic waste is revealed to a party, then they would be able to forge zk proofs for that system. The toxic waste is usually kept private in practice through the use of multi-party computation.

Older zk protocols like Pinocchio and Groth16 require a new trusted setup for each unique circuit. This creates problems whenever a project needs to update its circuits because then it would need to redo the trusted setup. Newer protocols like MARLIN and PLONK still require a trusted setup, but only once. These protocols’ setup is known as “universal” since they can be used for multiple programs, making upgrades much easier. Other protocols such as Bulletproofs and STARKs do not require trusted setups at all.

Example –> Zcash Trusted Setup (2016 – link)

During the original setup ceremony in 2016, there were extra elements produced mistakenly, violating the protocol’s “soundness”. See Appendix B

Some of these elements are unused by the prover and were included by mistake, but their presence allows a cheating prover to circumvent a consistency check, and thereby transform the proof of one statement into a valid-looking proof of a different statement. This breaks the soundness of the proving system.

The multi-party computation (MPC) protocol that produced Sprout parameters for the construction follows the paper’s setup procedure, including the computation of the extra elements. These are not included in the actual parameters distributed to Zcash nodes since they are omitted from the parameter file format used by the proving routine implementation in the libsnark library (used by Sprout). However, these elements do appear in the MPC ceremony transcript. Consequently, anyone with access to the ceremony transcript would have been able to create false proofs. –> I.e. print free money

Transcript – Used to verify the protocol’s evaluation and construct the proving/verifying keys.

The point of the transcript is to prove the linkage between the six participants’ publicly posted hashes of their part in the ceremony, and the resulting public parameters used in the Zcash 1.0 “Sprout” between October 2016 and October 2018. If you don’t have the transcript, you can’t verify that those six public hashes match the original parameters, which means someone (for example, someone who had hacked into one of the Zcash Company’s laptops) could have substituted their own parameters (for which they could have the toxic waste) in place of the parameters that the six participants collectively generated.