> For the complete documentation index, see [llms.txt](https://zokyo-auditing-tutorials.gitbook.io/zokyo-tutorials/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://zokyo-auditing-tutorials.gitbook.io/zokyo-tutorials/tutorial-16-zero-knowledge-zk/bugs-in-the-wild/ey-nightfall-missing-nullifier-range-check.md).

# EY Nightfall: Missing Nullifier Range Check

{% hint style="info" %}
[**Book an audit with Zokyo**](https://www.zokyo.io/)
{% endhint %}

**Summary**

Related Vulnerabilities: 3. [Arithmetic Over/Under flows](/zokyo-tutorials/tutorial-16-zero-knowledge-zk/common-vulnerabilities-in-zk-code/arithmetic-over-under-flows.md)

Identified By: [BlockHeader](https://github.com/BlockHeader)

EY Nightfall is a set of smart contracts and ZK circuits that allow users to transact ERC20 and ERC-721 tokens privately. The protocol requires that a nullifier be posted on-chain in order to spend private tokens. However, the protocol did not limit the range of the nullifier to the SNARK scalar field size. This allowed users to double spend tokens.

**Background**

In order to prevent double spending of private tokens, a nullifier is posted on-chain after the tokens are spent. If the nullifier was already present on-chain, then the tokens cannot be spent. The nullifier is computed in a deterministic way such that given the same input parameters (specific to the user’s private tokens in this case), the output nullifier will always be the same. The nullifier is stored on-chain as a 256 bit unsigned integer.

The EVM allows numbers up to 256 bits long, whereas the SNARK circuits used for Nightfall only allowed numbers up to around 254 bits long. Since the SNARK scalar field is 254 bits, a nullifier that is `> 254 bits` will be reduced modulo the SNARK field during the proof generation process. For example, let `p = SNARK scalar field order`. Then any number `x` in the proof generation process will be reduced to `x % p`. So `p + 1` will be reduced to 1.

**The Vulnerability**

The smart contract code that stores past used nullifiers did not check to ensure that the nullifier posted was within the SNARK scalar field (`< ~254 bits`). Since the circuit code is responsible for checking whether a given nullifier is correct or not for the tokens being spent, it will only check the reduced 254 bit version of the input nullifier.

For example, let's say a user wants to spend their tokens and the correct nullifier to do so is `n`. Since the correct nullifier is computed in the circuit code, `n` will be `< ~254 bits`. So the user can successfully spend the tokens by posting `n` on-chain as the nullifier. However, they can again post `n + p` on-chain, where `p = snark scalar field size`. Inside the circuit that checks whether `n + p` is correct, it will convert `n + p` to `n + p % p = n`. `n + p` essentially overflows to just `n`. So the circuit checks `n` and is therefore verified as the correct nullifier. On-chain, `n + p` and `n` are treated as two different nullifiers and don't overflow (unless `n + p > 256 bits`), so the nullifiers are stored separately and the tokens are spent a second time by the same user.

**The Fix**

The fix was to include a range check to ensure that any nullifiers posted on-chain were less than the SNARK scalar field size. This would prevent any overflows inside the circuit. Each token spend now only has one unique nullifier that can be posted on-chain successfully. Here is a snippet of the actual fix, where they ensure the nullifier is correctly range limited:

```
//checks to prevent a ZoKrates overflow attack
require(_inputs[3]<zokratesPrime, "Input too large - possible overflow attack");
require(_inputs[4]<zokratesPrime, "Input too large - possible overflow attack");
```

**Conclusion:**\
While the smart contract stored nullifiers as 256-bit unsigned integers, it failed to ensure these numbers were within the SNARK scalar field. This discrepancy allowed malicious actors to exploit the system. For instance, if a legitimate nullifier is "n", a user could post "n" on-chain to spend their tokens rightfully. But, taking advantage of the overflow mechanism, they could then post "n + p", where "p" represents the maximum size of the SNARK scalar field. Due to the modulo operation within the SNARK circuit, this number is reduced to "n", but on the blockchain, "n + p" and "n" are treated as distinct. This loophole essentially allowed tokens to be spent twice using different nullifiers that ultimately represented the same transaction.

\
To rectify the loophole, the Nightfall team introduced a range check ensuring that nullifiers posted on-chain adhered to the SNARK scalar field's size. By doing so, the potential for overflow within the SNARK circuit was eliminated. Consequently, every token spent is associated with a unique nullifier, preventing any future attempts at double-spending. The addition of simple checks that ensure the nullifiers are within the permissible range served as a protective measure against any potential overflow attacks.

**References**

1. [Github Issue](https://github.com/EYBlockchain/nightfall/issues/95)
2. [Github Fix](https://github.com/EYBlockchain/nightfall/pull/96)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://zokyo-auditing-tutorials.gitbook.io/zokyo-tutorials/tutorial-16-zero-knowledge-zk/bugs-in-the-wild/ey-nightfall-missing-nullifier-range-check.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
