Skip to content

Commit

Permalink
Merge pull request #1 from MisakaCenter/patch-1
Browse files Browse the repository at this point in the history
Add writeup for Tornado Crash
  • Loading branch information
tonyke-bot committed Dec 5, 2023
2 parents de7b1b1 + 0383bcd commit 935b686
Showing 1 changed file with 60 additions and 0 deletions.
60 changes: 60 additions & 0 deletions solutions/tornado-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Introduction
The 'Tornado Crash' challenge, rooted in the [Miximus](https://github.com/barryWhiteHat/miximus/tree/e774ef8524ba947285c671b946ce56f64b48c116) codebase, presented a unique vulnerability not in its cryptographic circuit but within the verifier contract.

# Background
In Miximus, a crucial component is using nullifiers for withdrawal processes. A nullifier is a key element designed to prevent double-spending or duplicate actions within blockchain transactions. When a user initiates a withdrawal, they are required to post a nullifier on-chain. This nullifier acts as a unique identifier for the transaction. If the nullifier has already been recorded on-chain, any subsequent transactions using the same nullifier are automatically rejected, thereby ensuring the integrity of the withdrawal process.

# Technical Details
The vulnerability in Miximus stemmed from a mismatch in data representation. The EVM handles numbers up to 256 bits, while the SNARK scalar field, integral to the cryptographic processes of Miximus, has a 254-bit limit (babyjubjub `p = 21888242871839275222246405745257275088548364400416034343698204186575808495617`). This discrepancy led to an overflow issue.

For instance, considering p as the SNARK scalar field order, any number x during the proof generation would be reduced to x % p. Consequently, a number like p + 1 would be reduced to 1.

## The Vulnerability
The core of the issue in the 'Tornado Crash' challenge was that the smart contract responsible for verifying the uniqueness of nullifiers did not check whether the nullifier was within the bounds of the SNARK scalar field. This oversight meant that if a user submitted a nullifier x greater than or equal to p, it could effectively be used twice: once as x and once as x % p. Both forms would be valid within the circuit and generate successful proofs.

This vulnerability allowed users to exploit the system by claiming the same transaction or action twice with two different yet technically valid nullifiers. When a user submitted a withdrawal with a nullifier x, since x had not been seen before, it was considered valid. Subsequently, the same withdrawal could be claimed with x % p, which, to the contract, appeared as a different, unseen nullifier, thus also passing validation.

In addition to submitting a nullifier x greater than or equal to p, users could also submit a nullifier less than p. This initially seems within the normal bounds of the system. However, the exploit was in adding p to this smaller nullifier, effectively creating a new, larger number which, when reduced modulo p, resulted in a different valid nullifier.

For instance, with a nullifier y such that y < p, the user could submit y and then submit y + p. Though y + p is technically a different number, when processed within the circuit, it reduces to y, due to the modulo operation. Therefore, the smart contract viewed y and y + p as two separate nullifiers, allowing the user to claim or execute the same action twice, despite technically using the same original nullifier in different mathematical forms.

# Solution

## Witness Generation
After making a deposit, participants could generate a witness using the following command:

```python
pk = genWitness(miximus, nullifier, sk, NUMBER_OF_DEPOSIT, tree_depth, fee, "../zksnark_element/pk.raw")
```

In this command, NUMBER_OF_DEPOSIT represents the global position of your deposit transaction.

The result of running this command would produce an output similar to:

```
"pk": {
"a": [...],
"a_p": [...],
"b": [...],
"b_p": [...],
"c": [...],
"c_p": [...],
"h": [...],
"k": [...],
"input": [
...,
...,
45826975940739001712875990737140574885322668875248592052086356330207943334071,
...,
...
]
}
```
In this output, the crucial element is `input[2]`, which in this case is `45826975940739001712875990737140574885322668875248592052086356330207943334071`. This value represents the nullifier.

## Constructing a New Valid Proof
By adding p (the SNARK scalar field order) to `input[2]`, a new nullifier could be constructed. This new nullifier, when submitted to the blockchain, would be distinct enough to bypass the smart contract's validation check, despite being derived from the same original nullifier. This exploit could be used to generate multiple valid proofs for the same deposit, effectively allowing participants to drain the pool.

# See also
1. [a16z ZkDrops: Missing Nullifier Range Check](https://github.com/0xPARC/zk-bug-tracker#9-a16z-zkdrops-missing-nullifier-range-check)
2. [EY Nightfall: Missing Nullifier Range Check](https://github.com/0xPARC/zk-bug-tracker#18-ey-nightfall-missing-nullifier-range-check)

0 comments on commit 935b686

Please sign in to comment.