Compare commits
215 Commits
Author | SHA1 | Date |
---|---|---|
Alexey Pertsev | 1ef6a263ac | |
Drygin | f9f19b70e4 | |
Drygin | 10aeb05417 | |
Roman Semenov | 896fc224ff | |
HowJMay | 0b8bbf6317 | |
poma | 7d924e2447 | |
poma | 2ec693d0de | |
poma | 0cf811b854 | |
poma | f468de9a0a | |
Roman Semenov | 94dfad9cd2 | |
poma | 23543683f3 | |
poma | 0c6e638852 | |
poma | 801f29a4b7 | |
mirru2532 | 5cb9d60178 | |
Alexey | 3603b1c9e1 | |
Alexey | c12643e2c2 | |
poma | 54a7bdcb04 | |
poma | f189a657c9 | |
poma | 3ad634594e | |
Alexey | 127a61e21f | |
Alexey | c559a79396 | |
Alexey | 78bd4175fa | |
Alexey | 4069b61421 | |
poma | f5d8f6d971 | |
poma | 8580c5e427 | |
poma | a359e86f85 | |
poma | 346ffcee3c | |
poma | c6b442713a | |
Roman Storm | 3c4def1e64 | |
Roman Semenov | 77af0c5bdd | |
Lucien Nocelli | 4f0a23426f | |
poma | b438f8db7b | |
Alexey Pertsev | d02ff4faa2 | |
Roman Storm | 1ad2158af1 | |
Roman Storm | 18e6a19800 | |
poma | 17308c9670 | |
Roman Storm | 5c3648cedc | |
Roman Storm | a0ef1a526d | |
Roman Semenov | fddd79d9bd | |
Alexey | 09baf9761d | |
Alexey | ea1435b115 | |
Alexey | 6d383235bb | |
Alexey | f48861a4f2 | |
Tsunami | 4408f39b5a | |
Alexey | c7d912c2e7 | |
Alexey | 4e120f26cb | |
Alexey | f90a898001 | |
Alexey | 03175a2277 | |
g. nicholas d'andrea | 7ceebf48d5 | |
Alexey | a533ad9ffb | |
poma | d4e6031982 | |
Alexey | 2d9677831a | |
Alexey | f04ff2b6fd | |
Alexey | 55e50fee3e | |
Alexey | b0bca7fc36 | |
Alexey | 54bb4c4b3c | |
Alexey | 411098b589 | |
Alexey | c8adb6b200 | |
Alexey | cfaf325c47 | |
Pertsev Alexey | 30b07f76a5 | |
Pertsev Alexey | d1c4a9bee6 | |
Alexey | 6a592154d2 | |
poma | 49bdd1bb6f | |
Alexey | 0e0ac72b0f | |
Alexey | 3e9df20f35 | |
Alexey | 62c7951961 | |
Alexey | 8afd208765 | |
Alexey | ff71072700 | |
Alexey | 27316a1edd | |
Alexey | 7033f34434 | |
Roman Semenov | 91e652ae66 | |
Alexey | a780956a63 | |
poma | 0fc3cda775 | |
poma | 079ba3aa5a | |
Roman Semenov | 0ac236d439 | |
Roman Storm | 9757978d27 | |
Roman Storm | 51c3d6b28c | |
Alexey | 6cd0ae8d87 | |
Alexey | 68a861ed35 | |
Alexey | ea93af3243 | |
Alexey | bb3c4aae52 | |
Roman Storm | 4a5921d8c2 | |
poma | 4114f7b52c | |
Pertsev Alexey | 8623e4092a | |
Pertsev Alexey | 2671aab17b | |
Alexey | 1be468c863 | |
Alexey | 265540067a | |
poma | 917892ef13 | |
Alexey | 2c28e1d5aa | |
poma | b6b8aa9619 | |
poma | ae44615198 | |
poma | 1515959a01 | |
Roman Storm | 5140e1b38f | |
Roman Semenov | 6ae2fe612c | |
poma | 2a0f0ccfd9 | |
poma | 0e9732625e | |
Roman Storm | 656673b690 | |
Roman Semenov | d9f4b16076 | |
Pertsev Alexey | beceeaae6e | |
poma | e83f528f6f | |
Roman Semenov | 62a9814aae | |
poma | 2962f30ed5 | |
Roman Storm | a6cda4a501 | |
Pertsev Alexey | f5486f0943 | |
Pertsev Alexey | 2de4f9c721 | |
poma | a58d60623b | |
poma | 0c4c27b7b6 | |
poma | c5d1ef7734 | |
Roman Storm | d0e312eb80 | |
Roman Storm | 93f8a8943e | |
poma | 24a21ac88f | |
poma | c0e81f2a37 | |
poma | e6a4208b52 | |
Alexey | ce550eea58 | |
Alexey | a6519fb280 | |
Alexey | 0393ee9c05 | |
Alexey | 5b60e44a7e | |
Roman Storm | df395187bf | |
Roman Storm | dec0f4487c | |
Roman Storm | a48858fd34 | |
Alexey | 83c9ba7296 | |
poma | d01018db9f | |
poma | 1a17e7eea3 | |
poma | 183847ad67 | |
Alexey | e6cce0c7ce | |
Roman Semenov | bc8d0b20fc | |
poma | 313713a061 | |
poma | a94281ccc5 | |
poma | c4dded8a20 | |
poma | 4d6dca78b2 | |
poma | 61864ceda1 | |
poma | ac8fc08cc2 | |
poma | 74913e67b2 | |
poma | e9c2055bb4 | |
poma | f783b45559 | |
poma | e710b243d7 | |
poma | 35f4b031f4 | |
poma | 3169d79ee0 | |
Roman Semenov | b000e66899 | |
Roman Semenov | fce4b1854c | |
Roman Semenov | a0a4050211 | |
Alexey | d5b16547f7 | |
poma | 1fd0c7fdea | |
poma | 27e3121bb0 | |
poma | ae889b5ad2 | |
poma | f8cd3fea1e | |
poma | c00e553299 | |
poma | 8a179b9217 | |
poma | 2ded1f8adb | |
poma | e413ccdc29 | |
poma | 1fdabcc97c | |
poma | 27a00bfd5f | |
poma | c92ac97ff2 | |
poma | 91adb03131 | |
poma | c47408ebd7 | |
poma | 6095106549 | |
poma | 6571f54768 | |
poma | 1364762b93 | |
poma | 2bb751bfd1 | |
poma | 48cc57fad7 | |
poma | 35500ac5bb | |
poma | 8e8243823a | |
poma | 111c966c1e | |
poma | 02e76a1ce6 | |
poma | d019e48da3 | |
poma | 54e2c5f890 | |
poma | b8d22464e3 | |
poma | 1d258715e0 | |
poma | b578213b0c | |
poma | bec65f217f | |
Roman Semenov | bc134bbfe8 | |
poma | ec4508e81e | |
poma | 07168f9816 | |
poma | 7193655e49 | |
poma | 9efab84e65 | |
Alexey | 0484408e82 | |
Alexey | a13a7306e2 | |
Roman Semenov | 63771560c6 | |
Roman Semenov | 4ce8d1c2ce | |
Agonical | dcbf2ab693 | |
poma | 0cbc0ad79b | |
poma | c889a88b4d | |
poma | a77c04ea5a | |
poma | c7f0ca9dfa | |
poma | 7a184d67d2 | |
poma | 55b3644fd7 | |
poma | 6035255a49 | |
poma | 5c3c78e097 | |
poma | 3624cb7531 | |
poma | 71b767ade1 | |
poma | 6b067f067f | |
poma | bb80cd9788 | |
Roman Storm | 5ef6e33c78 | |
Alexey | 9e7aa186dc | |
Alexey | 9132aeb6d5 | |
Roman Storm | a64f41a44e | |
Roman Storm | 13e01755a6 | |
Alexey | 0689d76df1 | |
Alexey | 3404acef48 | |
Alexey | 3f4e686899 | |
Pertsev Alexey | e75740becb | |
Pertsev Alexey | 7ed13aa8e0 | |
Alexey | 010837f92b | |
poma | 9009b9c56d | |
Roman Storm | 50872ac342 | |
poma | d39eb3ca4b | |
Drygin | 374dd420f5 | |
Drygin | ec30e2d357 | |
Alexey | 0e0c87d533 | |
Alexey | e0ec575745 | |
Alexey | b8c0c1898f | |
Alexey | 5006006a20 | |
Alexey | 9f33aadd9d | |
Alexey | 0f5a5df522 | |
Alexey | b8142d03bb |
16
.env.example
16
.env.example
|
@ -1,5 +1,15 @@
|
|||
MERKLE_TREE_HEIGHT=16
|
||||
MERKLE_TREE_HEIGHT=20
|
||||
# in wei
|
||||
AMOUNT=1000000000000000000
|
||||
EMPTY_ELEMENT=1337
|
||||
ETH_AMOUNT=100000000000000000
|
||||
TOKEN_AMOUNT=100000000000000000
|
||||
PRIVATE_KEY=
|
||||
ERC20_TOKEN=
|
||||
|
||||
# DAI mirror in Kovan
|
||||
#ERC20_TOKEN=0xd2b1a6b34f4a68425e7c28b4db5a37be3b7a4947
|
||||
# the block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some DAI is 13146218
|
||||
|
||||
# USDT mirror in Kovan
|
||||
#ERC20_TOKEN=0xf3e0d7bf58c5d455d31ef1c2d5375904df525105
|
||||
#TOKEN_AMOUNT=1000000
|
||||
# the block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some USDT is 13147586
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "prettier"],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"require-await": "error",
|
||||
"prettier/prettier": ["error", { "printWidth": 110 }]
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"require-await": "error"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['*']
|
||||
tags: ['v[0-9]+.[0-9]+.[0-9]+']
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- run: yarn install
|
||||
- run: yarn download
|
||||
- run: cp .env.example .env
|
||||
- run: npx ganache-cli > /dev/null &
|
||||
- run: npm run migrate:dev
|
||||
- run: yarn test
|
||||
- run: node src/cli.js test
|
||||
- run: yarn lint
|
||||
- run: yarn coverage
|
||||
- name: Coveralls
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Telegram Failure Notification
|
||||
uses: appleboy/telegram-action@0.0.7
|
||||
if: failure()
|
||||
with:
|
||||
message: ❗ Build failed for [${{ github.repository }}](https://github.com/${{ github.repository }}/actions) because of ${{ github.actor }}
|
||||
format: markdown
|
||||
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
@ -1,7 +1,7 @@
|
|||
build
|
||||
.vscode
|
||||
/index.js
|
||||
Mixer_flat.sol
|
||||
Tornado_flat.sol
|
||||
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
|
@ -94,3 +94,7 @@ typings/
|
|||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
ERC20Tornado_flat.sol
|
||||
ETHTornado_flat.sol
|
||||
|
||||
coverage.json
|
|
@ -0,0 +1,9 @@
|
|||
.vscode
|
||||
build
|
||||
circuits
|
||||
contracts/Verifier.sol
|
||||
scripts/ganacheHelper.js
|
||||
cli.js
|
||||
index.js
|
||||
coverage
|
||||
coverage.json
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"semi": false,
|
||||
"printWidth": 110,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.sol",
|
||||
"options": {
|
||||
"singleQuote": false,
|
||||
"printWidth": 130
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,6 +1,13 @@
|
|||
{
|
||||
"extends": "solhint:recommended",
|
||||
"rules": {
|
||||
"indent": ["error", 2]
|
||||
}
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"printWidth": 110
|
||||
}
|
||||
],
|
||||
"quotes": ["error", "double"]
|
||||
},
|
||||
"plugins": ["prettier"]
|
||||
}
|
||||
|
|
15
.travis.yml
15
.travis.yml
|
@ -1,15 +0,0 @@
|
|||
dist: trusty
|
||||
language: node_js
|
||||
node_js:
|
||||
- "11"
|
||||
install:
|
||||
- npm ci
|
||||
- cp .env.example .env
|
||||
- travis_wait 30 npm run build:circuit
|
||||
- npm run build:contract
|
||||
- npx ganache-cli > /dev/null &
|
||||
- npm run migrate:dev
|
||||
script:
|
||||
- npm run test
|
||||
- npm run eslint
|
||||
- ./cli.js auto
|
186
README.md
186
README.md
|
@ -1,47 +1,179 @@
|
|||
# Tornado mixer [![Build Status](https://travis-ci.org/peppersec/tornado-mixer.svg?branch=master)](https://travis-ci.org/peppersec/tornado-mixer)
|
||||
# Tornado Cash Privacy Solution [![build status](https://github.com/tornadocash/tornado-core/actions/workflows/build.yml/badge.svg)](https://github.com/tornadocash/tornado-core/actions/workflows/build.yml) [![Coverage Status](https://coveralls.io/repos/github/tornadocash/tornado-core/badge.svg?branch=master)](https://coveralls.io/github/tornadocash/tornado-core?branch=master)
|
||||
|
||||
![mixer image](./mixer.png)
|
||||
Tornado Cash is a non-custodial Ethereum and ERC20 privacy solution based on zkSNARKs. It improves transaction privacy by breaking the on-chain link between the recipient and destination addresses. It uses a smart contract that accepts ETH deposits that can be withdrawn by a different address. Whenever ETH is withdrawn by the new address, there is no way to link the withdrawal to the deposit, ensuring complete privacy.
|
||||
|
||||
To make a deposit user generates a secret and sends its hash (called a commitment) along with the deposit amount to the Tornado smart contract. The contract accepts the deposit and adds the commitment to its list of deposits.
|
||||
|
||||
Later, the user decides to make a withdrawal. To do that, the user should provide a proof that he or she possesses a secret to an unspent commitment from the smart contract’s list of deposits. zkSnark technology allows that to happen without revealing which exact deposit corresponds to this secret. The smart contract will check the proof and transfer deposited funds to the address specified for withdrawal. An external observer will be unable to determine which deposit this withdrawal came from.
|
||||
|
||||
You can read more about it in [this Medium article](https://medium.com/@tornado.cash/introducing-private-transactions-on-ethereum-now-42ee915babe0)
|
||||
|
||||
## Specs
|
||||
- Deposit gas cost: deposit 888054
|
||||
- Withdraw gas cost: 692133
|
||||
- Circuit constraints: 22617
|
||||
- Circuit proving time: 6116ms
|
||||
|
||||
- Deposit gas cost: 1088354 (43381 + 50859 \* tree_depth)
|
||||
- Withdraw gas cost: 301233
|
||||
- Circuit Constraints = 28271 (1869 + 1325 \* tree_depth)
|
||||
- Circuit Proof time = 10213ms (1071 + 347 \* tree_depth)
|
||||
- Serverless
|
||||
|
||||
## Security risks
|
||||
* Cryptographic tools used by mixer (zkSNARKS, Pedersen commitment, MiMC hash) are not yet extensively audited by cryptographic experts and may be vulnerable
|
||||
* Note: we use MiMC hash only for merkle tree, so even if a preimage attack on MiMC is discovered, it will not allow to deanonymize users. To drain funds attacker needs to be able to generate arbitrary hash collisions, which is a pretty strong assumption.
|
||||
* Relayer is frontrunnable. When relayer submits a transaction someone can see it in tx pool and frontrun it with higher gas price to get the fee and drain relayer funds.
|
||||
* Workaround: we can set high gas price so that (almost) all fee is used on gas. The relayer will not receive profit this way, but this approach is acceptable until we develop more sophisticated system that prevents frontrunning
|
||||
* Bugs in contract. Even though we have an extensive experience in smart contract security audits, we can still make mistakes. An external audit is needed to reduce probablility of bugs
|
||||
* ~~Nullifier griefing. when you submit a withdraw transaction you reveal the nullifier for your note. If someone manages to
|
||||
make a deposit with the same nullifier and withdraw it while your transaction is still in tx pool, your note will be considered
|
||||
spent since it has the same nullifier and it will prevent you from withdrawing your funds~~
|
||||
* Fixed by sending nullifier hash instead of plain nullifier
|
||||
![image](docs/diagram.png)
|
||||
|
||||
## Whitepaper
|
||||
|
||||
**[TornadoCash_whitepaper_v1.4.pdf](https://tornado.cash/audits/TornadoCash_whitepaper_v1.4.pdf)**
|
||||
|
||||
## Was it audited?
|
||||
|
||||
Tornado.cash protocols, circuits, and smart contracts were audited by a group of experts from [ABDK Consulting](https://www.abdk.consulting), specializing in zero-knowledge, cryptography, and smart contracts.
|
||||
|
||||
During the audit, no critical issues were found and all outstanding issues were fixed. The results can be found here:
|
||||
|
||||
- Cryptographic review https://tornado.cash/audits/TornadoCash_cryptographic_review_ABDK.pdf
|
||||
- Smart contract audit https://tornado.cash/audits/TornadoCash_contract_audit_ABDK.pdf
|
||||
- Zk-SNARK circuits audit https://tornado.cash/audits/TornadoCash_circuit_audit_ABDK.pdf
|
||||
|
||||
Underlying circomlib dependency is currently being audited, and the team already published most of the fixes for found issues
|
||||
|
||||
## Requirements
|
||||
|
||||
1. `node v11.15.0`
|
||||
2. `npm install -g npx`
|
||||
|
||||
## Usage
|
||||
1. `npm i`
|
||||
|
||||
You can see example usage in cli.js, it works both in the console and in the browser.
|
||||
|
||||
1. `npm install`
|
||||
1. `cp .env.example .env`
|
||||
1. `npm run build:circuit` - may take 10 minutes or more
|
||||
1. `npm run build:contract`
|
||||
1. `npm run browserify`
|
||||
1. `npm run build` - this may take 10 minutes or more
|
||||
1. `npx ganache-cli`
|
||||
1. `npm run test` - optionally run tests. It may fail for the first time, just run one more time.
|
||||
1. `npm run migrate:dev`
|
||||
1. `./cli.js deposit`
|
||||
1. `./cli.js withdraw <note from previous step> <destination eth address>`
|
||||
1. `./cli.js balance <destination eth address>`
|
||||
1. `npm run test` - optionally runs tests. It may fail on the first try, just run it again.
|
||||
|
||||
Use browser version on Kovan:
|
||||
|
||||
1. `vi .env` - add your Kovan private key to deploy contracts
|
||||
1. `npm run migrate`
|
||||
1. `npx http-server` - serve current dir, you can use any other http server
|
||||
1. `npx http-server` - serve current dir, you can use any other static http server
|
||||
1. Open `localhost:8080`
|
||||
|
||||
Use the command-line version. Works for Ganache, Kovan, and Mainnet:
|
||||
|
||||
### Initialization
|
||||
|
||||
1. `cp .env.example .env`
|
||||
1. `npm run download`
|
||||
1. `npm run build:contract`
|
||||
|
||||
### Ganache
|
||||
|
||||
1. make sure you complete steps from Initialization
|
||||
1. `ganache-cli -i 1337`
|
||||
1. `npm run migrate:dev`
|
||||
1. `./cli.js test`
|
||||
1. `./cli.js --help`
|
||||
|
||||
### Kovan, Mainnet
|
||||
|
||||
1. Please use https://github.com/tornadocash/tornado-cli
|
||||
Reason: because tornado-core uses websnark `2041cfa5fa0b71cd5cca9022a4eeea4afe28c9f7` commit hash in order to work with local trusted setup. Tornado-cli uses `4c0af6a8b65aabea3c09f377f63c44e7a58afa6d` commit with production trusted setup of tornadoCash
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
./cli.js deposit ETH 0.1 --rpc https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448
|
||||
```
|
||||
|
||||
> Your note: tornado-eth-0.1-42-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652
|
||||
> Tornado ETH balance is 8.9
|
||||
> Sender account ETH balance is 1004873.470619891361352542
|
||||
> Submitting deposit transaction
|
||||
> Tornado ETH balance is 9
|
||||
> Sender account ETH balance is 1004873.361652048361352542
|
||||
|
||||
```bash
|
||||
./cli.js withdraw tornado-eth-0.1-42-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 0x8589427373D6D84E98730D7795D8f6f8731FDA16 --rpc https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448 --relayer https://kovan-frelay.duckdns.org
|
||||
```
|
||||
|
||||
> Relay address: 0x6A31736e7490AbE5D5676be059DFf064AB4aC754
|
||||
> Getting current state from tornado contract
|
||||
> Generating SNARK proof
|
||||
> Proof time: 9117.051ms
|
||||
> Sending withdraw transaction through the relay
|
||||
> Transaction submitted through the relay. View transaction on etherscan https://kovan.etherscan.io/tx/0xcb21ae8cad723818c6bc7273e83e00c8393fcdbe74802ce5d562acad691a2a7b
|
||||
> Transaction mined in block 17036120
|
||||
> Done
|
||||
|
||||
## Deploy ETH Tornado Cash
|
||||
|
||||
1. `cp .env.example .env`
|
||||
1. Tune all necessary params
|
||||
1. `npx truffle migrate --network kovan --reset --f 2 --to 4`
|
||||
|
||||
## Deploy ERC20 Tornado Cash
|
||||
|
||||
1. `cp .env.example .env`
|
||||
1. Tune all necessary params
|
||||
1. `npx truffle migrate --network kovan --reset --f 2 --to 3`
|
||||
1. `npx truffle migrate --network kovan --reset --f 5`
|
||||
|
||||
**Note**. If you want to reuse the same verifier for all the instances, then after you deployed one of the instances you should only run the 4th or 5th migration for ETH or ERC20 contracts respectively (`--f 4 --to 4` or `--f 5`).
|
||||
|
||||
## How to resolve ENS name to DNS name for a relayer
|
||||
|
||||
1. Visit https://etherscan.io/enslookup and put relayer ENS name to the form.
|
||||
2. Copy the namehash (1) and click on the `Resolver` link (2)
|
||||
![enslookup](docs/enslookup.png)
|
||||
3. Go to the `Contract` tab. Click on `Read Contract` and scroll down to the `5. text` method.
|
||||
4. Put the values:
|
||||
![resolver](docs/resolver.png)
|
||||
5. Click `Query` and you will get the DNS name. Just add `https://` to it and use it as `relayer url`
|
||||
|
||||
## Credits
|
||||
|
||||
Special thanks to @barryWhiteHat and @kobigurk for valuable input,
|
||||
and to @jbaylina for awesome [Circom](https://github.com/iden3/circom) & [Websnark](https://github.com/iden3/websnark) framework
|
||||
and @jbaylina for awesome [Circom](https://github.com/iden3/circom) & [Websnark](https://github.com/iden3/websnark) framework
|
||||
|
||||
## Minimal demo example
|
||||
|
||||
1. `npm i`
|
||||
1. `ganache-cli -d`
|
||||
1. `npm run download`
|
||||
1. `npm run build:contract`
|
||||
1. `cp .env.example .env`
|
||||
1. `npm run migrate:dev`
|
||||
1. `node minimal-demo.js`
|
||||
|
||||
## Run tests/coverage
|
||||
|
||||
Prepare test environment:
|
||||
|
||||
```
|
||||
yarn install
|
||||
yarn download
|
||||
cp .env.example .env
|
||||
npx ganache-cli > /dev/null &
|
||||
npm run migrate:dev
|
||||
```
|
||||
|
||||
Run tests:
|
||||
|
||||
```
|
||||
yarn test
|
||||
```
|
||||
|
||||
Run coverage:
|
||||
|
||||
```
|
||||
yarn coverage
|
||||
```
|
||||
|
||||
## Emulate MPC trusted setup ceremony
|
||||
|
||||
```bash
|
||||
cargo install zkutil
|
||||
npx circom circuits/withdraw.circom -o build/circuits/withdraw.json
|
||||
zkutil setup -c build/circuits/withdraw.json -p build/circuits/withdraw.params
|
||||
zkutil export-keys -c build/circuits/withdraw.json -p build/circuits/withdraw.params -r build/circuits/withdraw_proving_key.json -v build/circuits/withdraw_verification_key.json
|
||||
zkutil generate-verifier -p build/circuits/withdraw.params -v build/circuits/Verifier.sol
|
||||
sed -i -e 's/pragma solidity \^0.6.0/pragma solidity 0.5.17/g' ./build/circuits/Verifier.sol
|
||||
```
|
||||
|
|
|
@ -1,72 +1,50 @@
|
|||
include "../node_modules/circomlib/circuits/mimcsponge.circom";
|
||||
|
||||
// Computes MiMC(left + right)
|
||||
template HashLeftRight(rounds) {
|
||||
// Computes MiMC([left, right])
|
||||
template HashLeftRight() {
|
||||
signal input left;
|
||||
signal input right;
|
||||
|
||||
signal output hash;
|
||||
|
||||
component hasher = MiMCSponge(2, rounds, 1);
|
||||
component hasher = MiMCSponge(2, 1);
|
||||
hasher.ins[0] <== left;
|
||||
hasher.ins[1] <== right;
|
||||
hasher.k <== 0;
|
||||
|
||||
hash <== hasher.outs[0];
|
||||
}
|
||||
|
||||
// if pathIndex == 0 returns (left = inputElement, right = pathElement)
|
||||
// if pathIndex == 1 returns (left = pathElement, right = inputElement)
|
||||
template Selector() {
|
||||
signal input inputElement;
|
||||
signal input pathElement;
|
||||
signal input pathIndex;
|
||||
// if s == 0 returns [in[0], in[1]]
|
||||
// if s == 1 returns [in[1], in[0]]
|
||||
template DualMux() {
|
||||
signal input in[2];
|
||||
signal input s;
|
||||
signal output out[2];
|
||||
|
||||
signal output left;
|
||||
signal output right;
|
||||
|
||||
signal leftSelector1;
|
||||
signal leftSelector2;
|
||||
signal rightSelector1;
|
||||
signal rightSelector2;
|
||||
|
||||
pathIndex * (1-pathIndex) === 0
|
||||
|
||||
leftSelector1 <== (1 - pathIndex) * inputElement;
|
||||
leftSelector2 <== (pathIndex) * pathElement;
|
||||
rightSelector1 <== (pathIndex) * inputElement;
|
||||
rightSelector2 <== (1 - pathIndex) * pathElement;
|
||||
|
||||
left <== leftSelector1 + leftSelector2;
|
||||
right <== rightSelector1 + rightSelector2;
|
||||
s * (1 - s) === 0
|
||||
out[0] <== (in[1] - in[0])*s + in[0];
|
||||
out[1] <== (in[0] - in[1])*s + in[1];
|
||||
}
|
||||
|
||||
// Verifies that merkle proof is correct for given merkle root and a leaf
|
||||
// pathIndex input is an array of 0/1 selectors telling whether given pathElement is on the left or right side of merkle path
|
||||
template MerkleTree(levels, rounds) {
|
||||
// pathIndices input is an array of 0/1 selectors telling whether given pathElement is on the left or right side of merkle path
|
||||
template MerkleTreeChecker(levels) {
|
||||
signal input leaf;
|
||||
signal input root;
|
||||
signal private input pathElements[levels];
|
||||
signal private input pathIndex[levels];
|
||||
signal input pathElements[levels];
|
||||
signal input pathIndices[levels];
|
||||
|
||||
component selectors[levels];
|
||||
component hashers[levels];
|
||||
|
||||
for (var i = 0; i < levels; i++) {
|
||||
selectors[i] = Selector();
|
||||
hashers[i] = HashLeftRight(rounds);
|
||||
selectors[i] = DualMux();
|
||||
selectors[i].in[0] <== i == 0 ? leaf : hashers[i - 1].hash;
|
||||
selectors[i].in[1] <== pathElements[i];
|
||||
selectors[i].s <== pathIndices[i];
|
||||
|
||||
selectors[i].pathElement <== pathElements[i];
|
||||
selectors[i].pathIndex <== pathIndex[i];
|
||||
|
||||
hashers[i].left <== selectors[i].left;
|
||||
hashers[i].right <== selectors[i].right;
|
||||
}
|
||||
|
||||
selectors[0].inputElement <== leaf;
|
||||
|
||||
for (var i = 1; i < levels; i++) {
|
||||
selectors[i].inputElement <== hashers[i-1].hash;
|
||||
hashers[i] = HashLeftRight();
|
||||
hashers[i].left <== selectors[i].out[0];
|
||||
hashers[i].right <== selectors[i].out[1];
|
||||
}
|
||||
|
||||
root === hashers[levels - 1].hash;
|
||||
|
|
|
@ -4,9 +4,8 @@ include "merkleTree.circom";
|
|||
|
||||
// computes Pedersen(nullifier + secret)
|
||||
template CommitmentHasher() {
|
||||
signal private input nullifier;
|
||||
signal private input secret;
|
||||
|
||||
signal input nullifier;
|
||||
signal input secret;
|
||||
signal output commitment;
|
||||
signal output nullifierHash;
|
||||
|
||||
|
@ -27,37 +26,42 @@ template CommitmentHasher() {
|
|||
}
|
||||
|
||||
// Verifies that commitment that corresponds to given secret and nullifier is included in the merkle tree of deposits
|
||||
template Withdraw(levels, rounds) {
|
||||
template Withdraw(levels) {
|
||||
signal input root;
|
||||
signal input nullifierHash;
|
||||
signal input receiver; // not taking part in any computations
|
||||
signal input fee; // not taking part in any computations
|
||||
signal input recipient; // not taking part in any computations
|
||||
signal input relayer; // not taking part in any computations
|
||||
signal input fee; // not taking part in any computations
|
||||
signal input refund; // not taking part in any computations
|
||||
signal private input nullifier;
|
||||
signal private input secret;
|
||||
signal private input pathElements[levels];
|
||||
signal private input pathIndex[levels];
|
||||
signal private input pathIndices[levels];
|
||||
|
||||
component hasher = CommitmentHasher();
|
||||
hasher.nullifier <== nullifier;
|
||||
hasher.secret <== secret;
|
||||
hasher.nullifierHash === nullifierHash;
|
||||
|
||||
nullifierHash === hasher.nullifierHash;
|
||||
|
||||
component tree = MerkleTree(levels, rounds);
|
||||
component tree = MerkleTreeChecker(levels);
|
||||
tree.leaf <== hasher.commitment;
|
||||
tree.root <== root;
|
||||
for (var i = 0; i < levels; i++) {
|
||||
tree.pathElements[i] <== pathElements[i];
|
||||
tree.pathIndex[i] <== pathIndex[i];
|
||||
tree.pathIndices[i] <== pathIndices[i];
|
||||
}
|
||||
|
||||
// Add hidden signals to make sure that tampering with receiver or fee will invalidate the snark proof
|
||||
// Add hidden signals to make sure that tampering with recipient or fee will invalidate the snark proof
|
||||
// Most likely it is not required, but it's better to stay on the safe side and it only takes 2 constraints
|
||||
// Squares are used to prevent optimizer from removing those constraints
|
||||
signal receiverSquare;
|
||||
signal recipientSquare;
|
||||
signal feeSquare;
|
||||
receiverSquare <== receiver * receiver;
|
||||
signal relayerSquare;
|
||||
signal refundSquare;
|
||||
recipientSquare <== recipient * recipient;
|
||||
feeSquare <== fee * fee;
|
||||
relayerSquare <== relayer * relayer;
|
||||
refundSquare <== refund * refund;
|
||||
}
|
||||
|
||||
component main = Withdraw(16, 220);
|
||||
component main = Withdraw(20);
|
||||
|
|
200
cli.js
200
cli.js
|
@ -1,200 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
// Temporary demo client
|
||||
// Works both in browser and node.js
|
||||
const fs = require('fs')
|
||||
const assert = require('assert')
|
||||
const snarkjs = require('snarkjs')
|
||||
const crypto = require('crypto')
|
||||
const circomlib = require('circomlib')
|
||||
const bigInt = snarkjs.bigInt
|
||||
const merkleTree = require('./lib/MerkleTree')
|
||||
const Web3 = require('web3')
|
||||
const buildGroth16 = require('websnark/src/groth16')
|
||||
const websnarkUtils = require('websnark/src/utils')
|
||||
|
||||
let web3, mixer, circuit, proving_key, groth16
|
||||
let MERKLE_TREE_HEIGHT, AMOUNT, EMPTY_ELEMENT
|
||||
const inBrowser = (typeof window !== 'undefined')
|
||||
|
||||
const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
|
||||
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
|
||||
|
||||
function createDeposit(nullifier, secret) {
|
||||
let deposit = { nullifier, secret }
|
||||
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
|
||||
deposit.commitment = pedersenHash(deposit.preimage)
|
||||
return deposit
|
||||
}
|
||||
|
||||
async function deposit() {
|
||||
const deposit = createDeposit(rbigint(31), rbigint(31))
|
||||
|
||||
console.log('Submitting deposit transaction')
|
||||
await mixer.methods.deposit('0x' + deposit.commitment.toString(16)).send({ value: AMOUNT, from: (await web3.eth.getAccounts())[0], gas:1e6 })
|
||||
|
||||
const note = '0x' + deposit.preimage.toString('hex')
|
||||
console.log('Your note:', note)
|
||||
return note
|
||||
}
|
||||
|
||||
async function getBalance(receiver) {
|
||||
const balance = await web3.eth.getBalance(receiver)
|
||||
console.log('Balance is ', web3.utils.fromWei(balance))
|
||||
}
|
||||
|
||||
async function withdraw(note, receiver) {
|
||||
let buf = Buffer.from(note.slice(2), 'hex')
|
||||
let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62)))
|
||||
|
||||
console.log('Getting current state from mixer contract')
|
||||
const events = await mixer.getPastEvents('Deposit', { fromBlock: mixer.deployedBlock, toBlock: 'latest' })
|
||||
let leafIndex
|
||||
|
||||
const commitment = deposit.commitment.toString(16).padStart('66', '0x000000')
|
||||
const leaves = events
|
||||
.sort((a, b) => a.returnValues.leafIndex.sub(b.returnValues.leafIndex))
|
||||
.map(e => {
|
||||
if (e.returnValues.commitment.eq(commitment)) {
|
||||
leafIndex = e.returnValues.leafIndex.toNumber()
|
||||
}
|
||||
return e.returnValues.commitment
|
||||
})
|
||||
const tree = new merkleTree(MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, leaves)
|
||||
const validRoot = await mixer.methods.isKnownRoot(await tree.root()).call()
|
||||
const nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
|
||||
const nullifierHashToCheck = nullifierHash.toString(16).padStart('66', '0x000000')
|
||||
const isSpent = await mixer.methods.isSpent(nullifierHashToCheck).call()
|
||||
assert(validRoot === true)
|
||||
assert(isSpent === false)
|
||||
|
||||
assert(leafIndex >= 0)
|
||||
const { root, path_elements, path_index } = await tree.path(leafIndex)
|
||||
// Circuit input
|
||||
const input = {
|
||||
// public
|
||||
root: root,
|
||||
nullifierHash,
|
||||
receiver: bigInt(receiver),
|
||||
fee: bigInt(0),
|
||||
|
||||
// private
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: path_elements,
|
||||
pathIndex: path_index,
|
||||
}
|
||||
|
||||
console.log('Generating SNARK proof')
|
||||
console.time('Proof time')
|
||||
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
|
||||
console.timeEnd('Proof time')
|
||||
|
||||
console.log('Submitting withdraw transaction')
|
||||
await mixer.methods.withdraw(pi_a, pi_b, pi_c, publicSignals).send({ from: (await web3.eth.getAccounts())[0], gas: 1e6 })
|
||||
console.log('Done')
|
||||
}
|
||||
|
||||
async function init() {
|
||||
let contractJson
|
||||
if (inBrowser) {
|
||||
web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 })
|
||||
contractJson = await (await fetch('build/contracts/Mixer.json')).json()
|
||||
circuit = await (await fetch('build/circuits/withdraw.json')).json()
|
||||
proving_key = await (await fetch('build/circuits/withdraw_proving_key.bin')).arrayBuffer()
|
||||
MERKLE_TREE_HEIGHT = 16
|
||||
AMOUNT = 1e18
|
||||
EMPTY_ELEMENT = 0
|
||||
} else {
|
||||
web3 = new Web3('http://localhost:8545', null, { transactionConfirmationBlocks: 1 })
|
||||
contractJson = require('./build/contracts/Mixer.json')
|
||||
circuit = require('./build/circuits/withdraw.json')
|
||||
proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
|
||||
require('dotenv').config()
|
||||
MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT
|
||||
AMOUNT = process.env.AMOUNT
|
||||
EMPTY_ELEMENT = process.env.EMPTY_ELEMENT
|
||||
}
|
||||
groth16 = await buildGroth16()
|
||||
let netId = await web3.eth.net.getId()
|
||||
const tx = await web3.eth.getTransaction(contractJson.networks[netId].transactionHash)
|
||||
mixer = new web3.eth.Contract(contractJson.abi, contractJson.networks[netId].address)
|
||||
mixer.deployedBlock = tx.blockNumber
|
||||
console.log('Loaded')
|
||||
}
|
||||
|
||||
// ========== CLI related stuff below ==============
|
||||
|
||||
function printHelp(code = 0) {
|
||||
console.log(`Usage:
|
||||
Submit a deposit from default eth account and return the resulting note
|
||||
$ ./cli.js deposit
|
||||
|
||||
Withdraw a note to 'receiver' account
|
||||
$ ./cli.js withdraw <note> <receiver>
|
||||
|
||||
Check address balance
|
||||
$ ./cli.js balance <address>
|
||||
|
||||
Example:
|
||||
$ ./cli.js deposit
|
||||
...
|
||||
Your note: 0x1941fa999e2b4bfeec3ce53c2440c3bc991b1b84c9bb650ea19f8331baf621001e696487e2a2ee54541fa12f49498d71e24d00b1731a8ccd4f5f5126f3d9f400
|
||||
|
||||
$ ./cli.js withdraw 0x1941fa999e2b4bfeec3ce53c2440c3bc991b1b84c9bb650ea19f8331baf621001e696487e2a2ee54541fa12f49498d71e24d00b1731a8ccd4f5f5126f3d9f400 0xee6249BA80596A4890D1BD84dbf5E4322eA4E7f0
|
||||
`)
|
||||
process.exit(code)
|
||||
}
|
||||
|
||||
if (inBrowser) {
|
||||
window.deposit = deposit
|
||||
window.withdraw = async () => {
|
||||
const note = prompt('Enter the note to withdraw')
|
||||
const receiver = (await web3.eth.getAccounts())[0]
|
||||
await withdraw(note, receiver)
|
||||
}
|
||||
init()
|
||||
} else {
|
||||
const args = process.argv.slice(2)
|
||||
if (args.length === 0) {
|
||||
printHelp()
|
||||
} else {
|
||||
switch (args[0]) {
|
||||
case 'deposit':
|
||||
if (args.length === 1) {
|
||||
init().then(() => deposit()).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
|
||||
}
|
||||
else
|
||||
printHelp(1)
|
||||
break
|
||||
case 'balance':
|
||||
if (args.length === 2 && /^0x[0-9a-fA-F]{40}$/.test(args[1])) {
|
||||
init().then(() => getBalance(args[1])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
|
||||
} else
|
||||
printHelp(1)
|
||||
break
|
||||
case 'withdraw':
|
||||
if (args.length === 3 && /^0x[0-9a-fA-F]{124}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) {
|
||||
init().then(() => withdraw(args[1], args[2])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
|
||||
}
|
||||
else
|
||||
printHelp(1)
|
||||
break
|
||||
case 'auto':
|
||||
if (args.length === 1) {
|
||||
(async () => {
|
||||
await init()
|
||||
const note = await deposit()
|
||||
await withdraw(note, (await web3.eth.getAccounts())[0])
|
||||
process.exit(0)
|
||||
})()
|
||||
}
|
||||
else
|
||||
printHelp(1)
|
||||
break
|
||||
|
||||
default:
|
||||
printHelp(1)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
// https://tornado.cash
|
||||
/*
|
||||
* d888888P dP a88888b. dP
|
||||
* 88 88 d8' `88 88
|
||||
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
|
||||
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
|
||||
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
|
||||
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
|
||||
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
|
||||
*/
|
||||
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
import "./Tornado.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
|
||||
|
||||
contract ERC20Tornado is Tornado {
|
||||
using SafeERC20 for IERC20;
|
||||
IERC20 public token;
|
||||
|
||||
constructor(
|
||||
IVerifier _verifier,
|
||||
IHasher _hasher,
|
||||
uint256 _denomination,
|
||||
uint32 _merkleTreeHeight,
|
||||
IERC20 _token
|
||||
) Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight) {
|
||||
token = _token;
|
||||
}
|
||||
|
||||
function _processDeposit() internal override {
|
||||
require(msg.value == 0, "ETH value is supposed to be 0 for ERC20 instance");
|
||||
token.safeTransferFrom(msg.sender, address(this), denomination);
|
||||
}
|
||||
|
||||
function _processWithdraw(
|
||||
address payable _recipient,
|
||||
address payable _relayer,
|
||||
uint256 _fee,
|
||||
uint256 _refund
|
||||
) internal override {
|
||||
require(msg.value == _refund, "Incorrect refund amount received by the contract");
|
||||
|
||||
token.safeTransfer(_recipient, denomination - _fee);
|
||||
if (_fee > 0) {
|
||||
token.safeTransfer(_relayer, _fee);
|
||||
}
|
||||
|
||||
if (_refund > 0) {
|
||||
(bool success, ) = _recipient.call{ value: _refund }("");
|
||||
if (!success) {
|
||||
// let's return _refund back to the relayer
|
||||
_relayer.transfer(_refund);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// https://tornado.cash
|
||||
/*
|
||||
* d888888P dP a88888b. dP
|
||||
* 88 88 d8' `88 88
|
||||
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
|
||||
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
|
||||
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
|
||||
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
|
||||
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
|
||||
*/
|
||||
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
import "./Tornado.sol";
|
||||
|
||||
contract ETHTornado is Tornado {
|
||||
constructor(
|
||||
IVerifier _verifier,
|
||||
IHasher _hasher,
|
||||
uint256 _denomination,
|
||||
uint32 _merkleTreeHeight
|
||||
) Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight) {}
|
||||
|
||||
function _processDeposit() internal override {
|
||||
require(msg.value == denomination, "Please send `mixDenomination` ETH along with transaction");
|
||||
}
|
||||
|
||||
function _processWithdraw(
|
||||
address payable _recipient,
|
||||
address payable _relayer,
|
||||
uint256 _fee,
|
||||
uint256 _refund
|
||||
) internal override {
|
||||
// sanity checks
|
||||
require(msg.value == 0, "Message value is supposed to be zero for ETH instance");
|
||||
require(_refund == 0, "Refund value is supposed to be zero for ETH instance");
|
||||
|
||||
(bool success, ) = _recipient.call{ value: denomination - _fee }("");
|
||||
require(success, "payment to _recipient did not go thru");
|
||||
if (_fee > 0) {
|
||||
(success, ) = _relayer.call{ value: _fee }("");
|
||||
require(success, "payment to _relayer did not go thru");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,137 +1,160 @@
|
|||
// https://tornado.cash
|
||||
/*
|
||||
* d888888P dP a88888b. dP
|
||||
* 88 88 d8' `88 88
|
||||
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
|
||||
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
|
||||
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
|
||||
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
|
||||
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
|
||||
*/
|
||||
* d888888P dP a88888b. dP
|
||||
* 88 88 d8' `88 88
|
||||
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
|
||||
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
|
||||
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
|
||||
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
|
||||
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
|
||||
*/
|
||||
|
||||
pragma solidity ^0.5.8;
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
library MiMC {
|
||||
function MiMCSponge(uint256 in_xL, uint256 in_xR, uint256 in_k) public pure returns (uint256 xL, uint256 xR);
|
||||
interface IHasher {
|
||||
function MiMCSponge(uint256 in_xL, uint256 in_xR) external pure returns (uint256 xL, uint256 xR);
|
||||
}
|
||||
|
||||
contract MerkleTreeWithHistory {
|
||||
uint256 public levels;
|
||||
uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
|
||||
uint256 public constant ZERO_VALUE = 21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE
|
||||
IHasher public immutable hasher;
|
||||
|
||||
uint8 constant ROOT_HISTORY_SIZE = 100;
|
||||
uint256[] private _roots;
|
||||
uint256 public current_root = 0;
|
||||
uint32 public levels;
|
||||
|
||||
uint256[] private _filled_subtrees;
|
||||
uint256[] private _zeros;
|
||||
// the following variables are made public for easier testing and debugging and
|
||||
// are not supposed to be accessed in regular code
|
||||
|
||||
uint32 public next_index = 0;
|
||||
// filledSubtrees and roots could be bytes32[size], but using mappings makes it cheaper because
|
||||
// it removes index range check on every interaction
|
||||
mapping(uint256 => bytes32) public filledSubtrees;
|
||||
mapping(uint256 => bytes32) public roots;
|
||||
uint32 public constant ROOT_HISTORY_SIZE = 30;
|
||||
uint32 public currentRootIndex = 0;
|
||||
uint32 public nextIndex = 0;
|
||||
|
||||
constructor(uint256 tree_levels, uint256 zero_value) public {
|
||||
levels = tree_levels;
|
||||
constructor(uint32 _levels, IHasher _hasher) {
|
||||
require(_levels > 0, "_levels should be greater than zero");
|
||||
require(_levels < 32, "_levels should be less than 32");
|
||||
levels = _levels;
|
||||
hasher = _hasher;
|
||||
|
||||
_zeros.push(zero_value);
|
||||
_filled_subtrees.push(_zeros[0]);
|
||||
|
||||
for (uint8 i = 1; i < levels; i++) {
|
||||
_zeros.push(hashLeftRight(_zeros[i-1], _zeros[i-1]));
|
||||
_filled_subtrees.push(_zeros[i]);
|
||||
for (uint32 i = 0; i < _levels; i++) {
|
||||
filledSubtrees[i] = zeros(i);
|
||||
}
|
||||
|
||||
_roots = new uint256[](ROOT_HISTORY_SIZE);
|
||||
_roots[0] = hashLeftRight(_zeros[levels - 1], _zeros[levels - 1]);
|
||||
roots[0] = zeros(_levels - 1);
|
||||
}
|
||||
|
||||
function hashLeftRight(uint256 left, uint256 right) public pure returns (uint256 mimc_hash) {
|
||||
uint256 k = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
|
||||
uint256 R = 0;
|
||||
/**
|
||||
@dev Hash 2 tree leaves, returns MiMC(_left, _right)
|
||||
*/
|
||||
function hashLeftRight(
|
||||
IHasher _hasher,
|
||||
bytes32 _left,
|
||||
bytes32 _right
|
||||
) public pure returns (bytes32) {
|
||||
require(uint256(_left) < FIELD_SIZE, "_left should be inside the field");
|
||||
require(uint256(_right) < FIELD_SIZE, "_right should be inside the field");
|
||||
uint256 R = uint256(_left);
|
||||
uint256 C = 0;
|
||||
|
||||
R = addmod(R, left, k);
|
||||
(R, C) = MiMC.MiMCSponge(R, C, 0);
|
||||
|
||||
R = addmod(R, right, k);
|
||||
(R, C) = MiMC.MiMCSponge(R, C, 0);
|
||||
|
||||
mimc_hash = R;
|
||||
(R, C) = _hasher.MiMCSponge(R, C);
|
||||
R = addmod(R, uint256(_right), FIELD_SIZE);
|
||||
(R, C) = _hasher.MiMCSponge(R, C);
|
||||
return bytes32(R);
|
||||
}
|
||||
|
||||
function _insert(uint256 leaf) internal {
|
||||
uint32 current_index = next_index;
|
||||
require(current_index != 2**(levels - 1), "Merkle tree is full");
|
||||
next_index += 1;
|
||||
uint256 current_level_hash = leaf;
|
||||
uint256 left;
|
||||
uint256 right;
|
||||
function _insert(bytes32 _leaf) internal returns (uint32 index) {
|
||||
uint32 _nextIndex = nextIndex;
|
||||
require(_nextIndex != uint32(2)**levels, "Merkle tree is full. No more leaves can be added");
|
||||
uint32 currentIndex = _nextIndex;
|
||||
bytes32 currentLevelHash = _leaf;
|
||||
bytes32 left;
|
||||
bytes32 right;
|
||||
|
||||
for (uint256 i = 0; i < levels; i++) {
|
||||
if (current_index % 2 == 0) {
|
||||
left = current_level_hash;
|
||||
right = _zeros[i];
|
||||
|
||||
_filled_subtrees[i] = current_level_hash;
|
||||
for (uint32 i = 0; i < levels; i++) {
|
||||
if (currentIndex % 2 == 0) {
|
||||
left = currentLevelHash;
|
||||
right = zeros(i);
|
||||
filledSubtrees[i] = currentLevelHash;
|
||||
} else {
|
||||
left = _filled_subtrees[i];
|
||||
right = current_level_hash;
|
||||
left = filledSubtrees[i];
|
||||
right = currentLevelHash;
|
||||
}
|
||||
|
||||
current_level_hash = hashLeftRight(left, right);
|
||||
|
||||
current_index /= 2;
|
||||
currentLevelHash = hashLeftRight(hasher, left, right);
|
||||
currentIndex /= 2;
|
||||
}
|
||||
|
||||
current_root = (current_root + 1) % ROOT_HISTORY_SIZE;
|
||||
_roots[current_root] = current_level_hash;
|
||||
uint32 newRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
|
||||
currentRootIndex = newRootIndex;
|
||||
roots[newRootIndex] = currentLevelHash;
|
||||
nextIndex = _nextIndex + 1;
|
||||
return _nextIndex;
|
||||
}
|
||||
|
||||
function isKnownRoot(uint256 root) public view returns(bool) {
|
||||
if (root == 0) {
|
||||
/**
|
||||
@dev Whether the root is present in the root history
|
||||
*/
|
||||
function isKnownRoot(bytes32 _root) public view returns (bool) {
|
||||
if (_root == 0) {
|
||||
return false;
|
||||
}
|
||||
// search most recent first
|
||||
uint256 i;
|
||||
for(i = current_root; i < 2**256 - 1; i--) {
|
||||
if (root == _roots[i]) {
|
||||
uint32 _currentRootIndex = currentRootIndex;
|
||||
uint32 i = _currentRootIndex;
|
||||
do {
|
||||
if (_root == roots[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// process the rest of roots
|
||||
for(i = ROOT_HISTORY_SIZE - 1; i > current_root; i--) {
|
||||
if (root == _roots[i]) {
|
||||
return true;
|
||||
if (i == 0) {
|
||||
i = ROOT_HISTORY_SIZE;
|
||||
}
|
||||
}
|
||||
i--;
|
||||
} while (i != _currentRootIndex);
|
||||
return false;
|
||||
|
||||
// or we can do that in other way
|
||||
// uint256 i = _current_root;
|
||||
// do {
|
||||
// if (root == _roots[i]) {
|
||||
// return true;
|
||||
// }
|
||||
// if (i == 0) {
|
||||
// i = ROOT_HISTORY_SIZE;
|
||||
// }
|
||||
// i--;
|
||||
// } while (i != _current_root);
|
||||
}
|
||||
|
||||
function getLastRoot() public view returns(uint256) {
|
||||
return _roots[current_root];
|
||||
/**
|
||||
@dev Returns the last root
|
||||
*/
|
||||
function getLastRoot() public view returns (bytes32) {
|
||||
return roots[currentRootIndex];
|
||||
}
|
||||
|
||||
function roots() public view returns(uint256[] memory) {
|
||||
return _roots;
|
||||
}
|
||||
|
||||
function filled_subtrees() public view returns(uint256[] memory) {
|
||||
return _filled_subtrees;
|
||||
}
|
||||
|
||||
function zeros() public view returns(uint256[] memory) {
|
||||
return _zeros;
|
||||
/// @dev provides Zero (Empty) elements for a MiMC MerkleTree. Up to 32 levels
|
||||
function zeros(uint256 i) public pure returns (bytes32) {
|
||||
if (i == 0) return bytes32(0x2fe54c60d3acabf3343a35b6eba15db4821b340f76e741e2249685ed4899af6c);
|
||||
else if (i == 1) return bytes32(0x256a6135777eee2fd26f54b8b7037a25439d5235caee224154186d2b8a52e31d);
|
||||
else if (i == 2) return bytes32(0x1151949895e82ab19924de92c40a3d6f7bcb60d92b00504b8199613683f0c200);
|
||||
else if (i == 3) return bytes32(0x20121ee811489ff8d61f09fb89e313f14959a0f28bb428a20dba6b0b068b3bdb);
|
||||
else if (i == 4) return bytes32(0x0a89ca6ffa14cc462cfedb842c30ed221a50a3d6bf022a6a57dc82ab24c157c9);
|
||||
else if (i == 5) return bytes32(0x24ca05c2b5cd42e890d6be94c68d0689f4f21c9cec9c0f13fe41d566dfb54959);
|
||||
else if (i == 6) return bytes32(0x1ccb97c932565a92c60156bdba2d08f3bf1377464e025cee765679e604a7315c);
|
||||
else if (i == 7) return bytes32(0x19156fbd7d1a8bf5cba8909367de1b624534ebab4f0f79e003bccdd1b182bdb4);
|
||||
else if (i == 8) return bytes32(0x261af8c1f0912e465744641409f622d466c3920ac6e5ff37e36604cb11dfff80);
|
||||
else if (i == 9) return bytes32(0x0058459724ff6ca5a1652fcbc3e82b93895cf08e975b19beab3f54c217d1c007);
|
||||
else if (i == 10) return bytes32(0x1f04ef20dee48d39984d8eabe768a70eafa6310ad20849d4573c3c40c2ad1e30);
|
||||
else if (i == 11) return bytes32(0x1bea3dec5dab51567ce7e200a30f7ba6d4276aeaa53e2686f962a46c66d511e5);
|
||||
else if (i == 12) return bytes32(0x0ee0f941e2da4b9e31c3ca97a40d8fa9ce68d97c084177071b3cb46cd3372f0f);
|
||||
else if (i == 13) return bytes32(0x1ca9503e8935884501bbaf20be14eb4c46b89772c97b96e3b2ebf3a36a948bbd);
|
||||
else if (i == 14) return bytes32(0x133a80e30697cd55d8f7d4b0965b7be24057ba5dc3da898ee2187232446cb108);
|
||||
else if (i == 15) return bytes32(0x13e6d8fc88839ed76e182c2a779af5b2c0da9dd18c90427a644f7e148a6253b6);
|
||||
else if (i == 16) return bytes32(0x1eb16b057a477f4bc8f572ea6bee39561098f78f15bfb3699dcbb7bd8db61854);
|
||||
else if (i == 17) return bytes32(0x0da2cb16a1ceaabf1c16b838f7a9e3f2a3a3088d9e0a6debaa748114620696ea);
|
||||
else if (i == 18) return bytes32(0x24a3b3d822420b14b5d8cb6c28a574f01e98ea9e940551d2ebd75cee12649f9d);
|
||||
else if (i == 19) return bytes32(0x198622acbd783d1b0d9064105b1fc8e4d8889de95c4c519b3f635809fe6afc05);
|
||||
else if (i == 20) return bytes32(0x29d7ed391256ccc3ea596c86e933b89ff339d25ea8ddced975ae2fe30b5296d4);
|
||||
else if (i == 21) return bytes32(0x19be59f2f0413ce78c0c3703a3a5451b1d7f39629fa33abd11548a76065b2967);
|
||||
else if (i == 22) return bytes32(0x1ff3f61797e538b70e619310d33f2a063e7eb59104e112e95738da1254dc3453);
|
||||
else if (i == 23) return bytes32(0x10c16ae9959cf8358980d9dd9616e48228737310a10e2b6b731c1a548f036c48);
|
||||
else if (i == 24) return bytes32(0x0ba433a63174a90ac20992e75e3095496812b652685b5e1a2eae0b1bf4e8fcd1);
|
||||
else if (i == 25) return bytes32(0x019ddb9df2bc98d987d0dfeca9d2b643deafab8f7036562e627c3667266a044c);
|
||||
else if (i == 26) return bytes32(0x2d3c88b23175c5a5565db928414c66d1912b11acf974b2e644caaac04739ce99);
|
||||
else if (i == 27) return bytes32(0x2eab55f6ae4e66e32c5189eed5c470840863445760f5ed7e7b69b2a62600f354);
|
||||
else if (i == 28) return bytes32(0x002df37a2642621802383cf952bf4dd1f32e05433beeb1fd41031fb7eace979d);
|
||||
else if (i == 29) return bytes32(0x104aeb41435db66c3e62feccc1d6f5d98d0a0ed75d1374db457cf462e3a1f427);
|
||||
else if (i == 30) return bytes32(0x1f3c6fd858e9a7d4b0d1f38e256a09d81d5a5e3c963987e2d4b814cfab7c6ebb);
|
||||
else if (i == 31) return bytes32(0x2c7a07d20dff79d01fecedc1134284a8d08436606c93693b67e333f671bf69cc);
|
||||
else revert("Index out of bounds");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
pragma solidity >=0.4.21 <0.6.0;
|
||||
|
||||
contract Migrations {
|
||||
address public owner;
|
||||
uint public last_completed_migration;
|
||||
|
||||
constructor() public {
|
||||
owner = msg.sender;
|
||||
}
|
||||
|
||||
modifier restricted() {
|
||||
if (msg.sender == owner) _;
|
||||
}
|
||||
|
||||
function setCompleted(uint completed) public restricted {
|
||||
last_completed_migration = completed;
|
||||
}
|
||||
|
||||
function upgrade(address new_address) public restricted {
|
||||
Migrations upgraded = Migrations(new_address);
|
||||
upgraded.setCompleted(last_completed_migration);
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
// https://tornado.cash
|
||||
/*
|
||||
* d888888P dP a88888b. dP
|
||||
* 88 88 d8' `88 88
|
||||
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
|
||||
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
|
||||
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
|
||||
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
|
||||
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
|
||||
*/
|
||||
|
||||
pragma solidity ^0.5.8;
|
||||
|
||||
import "./MerkleTreeWithHistory.sol";
|
||||
|
||||
contract IVerifier {
|
||||
function verifyProof(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[4] memory input) public returns(bool);
|
||||
}
|
||||
|
||||
contract Mixer is MerkleTreeWithHistory {
|
||||
uint256 public transferValue;
|
||||
bool public isDepositsEnabled = true;
|
||||
// operator can disable new deposits in case of emergency
|
||||
// it also receives a relayer fee
|
||||
address payable public operator;
|
||||
mapping(uint256 => bool) public nullifierHashes;
|
||||
// we store all commitments just to prevent accidental deposits with the same commitment
|
||||
mapping(uint256 => bool) public commitments;
|
||||
IVerifier public verifier;
|
||||
|
||||
event Deposit(uint256 indexed commitment, uint256 leafIndex, uint256 timestamp);
|
||||
event Withdraw(address to, uint256 nullifierHash, uint256 fee);
|
||||
|
||||
/**
|
||||
@dev The constructor
|
||||
@param _verifier the address of SNARK verifier for this contract
|
||||
@param _transferValue the value for all deposits in this contract in wei
|
||||
*/
|
||||
constructor(
|
||||
address _verifier,
|
||||
uint256 _transferValue,
|
||||
uint8 _merkleTreeHeight,
|
||||
uint256 _emptyElement,
|
||||
address payable _operator
|
||||
) MerkleTreeWithHistory(_merkleTreeHeight, _emptyElement) public {
|
||||
verifier = IVerifier(_verifier);
|
||||
transferValue = _transferValue;
|
||||
operator = _operator;
|
||||
}
|
||||
|
||||
/**
|
||||
@dev Deposit funds into mixer. The caller must send value equal to `transferValue` of this mixer.
|
||||
@param commitment the note commitment, which is PedersenHash(nullifier + secret)
|
||||
*/
|
||||
function deposit(uint256 commitment) public payable {
|
||||
require(isDepositsEnabled, "deposits disabled");
|
||||
require(msg.value == transferValue, "Please send `transferValue` ETH along with transaction");
|
||||
require(!commitments[commitment], "The commitment has been submitted");
|
||||
_insert(commitment);
|
||||
commitments[commitment] = true;
|
||||
emit Deposit(commitment, next_index - 1, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
@dev Withdraw deposit from the mixer. `a`, `b`, and `c` are zkSNARK proof data, and input is an array of circuit public inputs
|
||||
`input` array consists of:
|
||||
- merkle root of all deposits in the mixer
|
||||
- hash of unique deposit nullifier to prevent double spends
|
||||
- the receiver of funds
|
||||
- optional fee that goes to the transaction sender (usually a relay)
|
||||
*/
|
||||
function withdraw(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[4] memory input) public {
|
||||
uint256 root = input[0];
|
||||
uint256 nullifierHash = input[1];
|
||||
address payable receiver = address(input[2]);
|
||||
uint256 fee = input[3];
|
||||
|
||||
require(!nullifierHashes[nullifierHash], "The note has been already spent");
|
||||
require(fee < transferValue, "Fee exceeds transfer value");
|
||||
require(isKnownRoot(root), "Cannot find your merkle root"); // Make sure to use a recent one
|
||||
require(verifier.verifyProof(a, b, c, input), "Invalid withdraw proof");
|
||||
|
||||
nullifierHashes[nullifierHash] = true;
|
||||
receiver.transfer(transferValue - fee);
|
||||
if (fee > 0) {
|
||||
operator.transfer(fee);
|
||||
}
|
||||
emit Withdraw(receiver, nullifierHash, fee);
|
||||
}
|
||||
|
||||
function toggleDeposits() external {
|
||||
require(msg.sender == operator, "unauthorized");
|
||||
isDepositsEnabled = !isDepositsEnabled;
|
||||
}
|
||||
|
||||
function changeOperator(address payable _newAccount) external {
|
||||
require(msg.sender == operator, "unauthorized");
|
||||
operator = _newAccount;
|
||||
}
|
||||
|
||||
function isSpent(uint256 nullifier) public view returns(bool) {
|
||||
return nullifierHashes[nullifier];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
contract BadRecipient {
|
||||
fallback() external {
|
||||
require(false, "this contract does not accept ETH");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
|
||||
contract ERC20Mock is ERC20("DAIMock", "DAIM") {
|
||||
function mint(address account, uint256 amount) public {
|
||||
_mint(account, amount);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
interface IDeployer {
|
||||
function deploy(bytes memory _initCode, bytes32 _salt) external returns (address payable createdContract);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
interface ERC20Basic {
|
||||
function _totalSupply() external returns (uint256);
|
||||
|
||||
function totalSupply() external view returns (uint256);
|
||||
|
||||
function balanceOf(address who) external view returns (uint256);
|
||||
|
||||
function transfer(address to, uint256 value) external;
|
||||
|
||||
event Transfer(address indexed from, address indexed to, uint256 value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title ERC20 interface
|
||||
* @dev see https://github.com/ethereum/EIPs/issues/20
|
||||
*/
|
||||
interface IUSDT is ERC20Basic {
|
||||
function allowance(address owner, address spender) external view returns (uint256);
|
||||
|
||||
function transferFrom(
|
||||
address from,
|
||||
address to,
|
||||
uint256 value
|
||||
) external;
|
||||
|
||||
function approve(address spender, uint256 value) external;
|
||||
|
||||
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
pragma solidity ^0.5.8;
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
import '../MerkleTreeWithHistory.sol';
|
||||
import "../MerkleTreeWithHistory.sol";
|
||||
|
||||
contract MerkleTreeWithHistoryMock is MerkleTreeWithHistory {
|
||||
constructor(uint32 _treeLevels, IHasher _hasher) MerkleTreeWithHistory(_treeLevels, _hasher) {}
|
||||
|
||||
constructor (uint8 tree_levels, uint256 zero_value) MerkleTreeWithHistory(tree_levels, zero_value) public {}
|
||||
|
||||
function insert(uint256 leaf) public {
|
||||
_insert(leaf);
|
||||
function insert(bytes32 _leaf) public {
|
||||
_insert(_leaf);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
// https://tornado.cash
|
||||
/*
|
||||
* d888888P dP a88888b. dP
|
||||
* 88 88 d8' `88 88
|
||||
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
|
||||
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
|
||||
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
|
||||
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
|
||||
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
|
||||
*/
|
||||
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
import "./MerkleTreeWithHistory.sol";
|
||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
|
||||
interface IVerifier {
|
||||
function verifyProof(bytes memory _proof, uint256[6] memory _input) external returns (bool);
|
||||
}
|
||||
|
||||
abstract contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
|
||||
IVerifier public immutable verifier;
|
||||
uint256 public denomination;
|
||||
|
||||
mapping(bytes32 => bool) public nullifierHashes;
|
||||
// we store all commitments just to prevent accidental deposits with the same commitment
|
||||
mapping(bytes32 => bool) public commitments;
|
||||
|
||||
event Deposit(bytes32 indexed commitment, uint32 leafIndex, uint256 timestamp);
|
||||
event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 fee);
|
||||
|
||||
/**
|
||||
@dev The constructor
|
||||
@param _verifier the address of SNARK verifier for this contract
|
||||
@param _hasher the address of MiMC hash contract
|
||||
@param _denomination transfer amount for each deposit
|
||||
@param _merkleTreeHeight the height of deposits' Merkle Tree
|
||||
*/
|
||||
constructor(
|
||||
IVerifier _verifier,
|
||||
IHasher _hasher,
|
||||
uint256 _denomination,
|
||||
uint32 _merkleTreeHeight
|
||||
) MerkleTreeWithHistory(_merkleTreeHeight, _hasher) {
|
||||
require(_denomination > 0, "denomination should be greater than 0");
|
||||
verifier = _verifier;
|
||||
denomination = _denomination;
|
||||
}
|
||||
|
||||
/**
|
||||
@dev Deposit funds into the contract. The caller must send (for ETH) or approve (for ERC20) value equal to or `denomination` of this instance.
|
||||
@param _commitment the note commitment, which is PedersenHash(nullifier + secret)
|
||||
*/
|
||||
function deposit(bytes32 _commitment) external payable nonReentrant {
|
||||
require(!commitments[_commitment], "The commitment has been submitted");
|
||||
|
||||
uint32 insertedIndex = _insert(_commitment);
|
||||
commitments[_commitment] = true;
|
||||
_processDeposit();
|
||||
|
||||
emit Deposit(_commitment, insertedIndex, block.timestamp);
|
||||
}
|
||||
|
||||
/** @dev this function is defined in a child contract */
|
||||
function _processDeposit() internal virtual;
|
||||
|
||||
/**
|
||||
@dev Withdraw a deposit from the contract. `proof` is a zkSNARK proof data, and input is an array of circuit public inputs
|
||||
`input` array consists of:
|
||||
- merkle root of all deposits in the contract
|
||||
- hash of unique deposit nullifier to prevent double spends
|
||||
- the recipient of funds
|
||||
- optional fee that goes to the transaction sender (usually a relay)
|
||||
*/
|
||||
function withdraw(
|
||||
bytes calldata _proof,
|
||||
bytes32 _root,
|
||||
bytes32 _nullifierHash,
|
||||
address payable _recipient,
|
||||
address payable _relayer,
|
||||
uint256 _fee,
|
||||
uint256 _refund
|
||||
) external payable nonReentrant {
|
||||
require(_fee <= denomination, "Fee exceeds transfer value");
|
||||
require(!nullifierHashes[_nullifierHash], "The note has been already spent");
|
||||
require(isKnownRoot(_root), "Cannot find your merkle root"); // Make sure to use a recent one
|
||||
require(
|
||||
verifier.verifyProof(
|
||||
_proof,
|
||||
[uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund]
|
||||
),
|
||||
"Invalid withdraw proof"
|
||||
);
|
||||
|
||||
nullifierHashes[_nullifierHash] = true;
|
||||
_processWithdraw(_recipient, _relayer, _fee, _refund);
|
||||
emit Withdrawal(_recipient, _nullifierHash, _relayer, _fee);
|
||||
}
|
||||
|
||||
/** @dev this function is defined in a child contract */
|
||||
function _processWithdraw(
|
||||
address payable _recipient,
|
||||
address payable _relayer,
|
||||
uint256 _fee,
|
||||
uint256 _refund
|
||||
) internal virtual;
|
||||
|
||||
/** @dev whether a note is already spent */
|
||||
function isSpent(bytes32 _nullifierHash) public view returns (bool) {
|
||||
return nullifierHashes[_nullifierHash];
|
||||
}
|
||||
|
||||
/** @dev whether an array of notes is already spent */
|
||||
function isSpentArray(bytes32[] calldata _nullifierHashes) external view returns (bool[] memory spent) {
|
||||
spent = new bool[](_nullifierHashes.length);
|
||||
for (uint256 i = 0; i < _nullifierHashes.length; i++) {
|
||||
if (isSpent(_nullifierHashes[i])) {
|
||||
spent[i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
../build/circuits/Verifier.sol
|
|
@ -0,0 +1,231 @@
|
|||
/**
|
||||
*Submitted for verification at Etherscan.io on 2020-05-12
|
||||
*/
|
||||
|
||||
// https://tornado.cash Verifier.sol generated by trusted setup ceremony.
|
||||
/*
|
||||
* d888888P dP a88888b. dP
|
||||
* 88 88 d8' `88 88
|
||||
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
|
||||
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
|
||||
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
|
||||
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
|
||||
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
|
||||
*/
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright 2017 Christian Reitwiessner
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// 2019 OKIMS
|
||||
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
library Pairing {
|
||||
uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
|
||||
|
||||
struct G1Point {
|
||||
uint256 X;
|
||||
uint256 Y;
|
||||
}
|
||||
|
||||
// Encoding of field elements is: X[0] * z + X[1]
|
||||
struct G2Point {
|
||||
uint256[2] X;
|
||||
uint256[2] Y;
|
||||
}
|
||||
|
||||
/*
|
||||
* @return The negation of p, i.e. p.plus(p.negate()) should be zero.
|
||||
*/
|
||||
function negate(G1Point memory p) internal pure returns (G1Point memory) {
|
||||
// The prime q in the base field F_q for G1
|
||||
if (p.X == 0 && p.Y == 0) {
|
||||
return G1Point(0, 0);
|
||||
} else {
|
||||
return G1Point(p.X, PRIME_Q - (p.Y % PRIME_Q));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @return r the sum of two points of G1
|
||||
*/
|
||||
function plus(
|
||||
G1Point memory p1,
|
||||
G1Point memory p2
|
||||
) internal view returns (G1Point memory r) {
|
||||
uint256[4] memory input;
|
||||
input[0] = p1.X;
|
||||
input[1] = p1.Y;
|
||||
input[2] = p2.X;
|
||||
input[3] = p2.Y;
|
||||
bool success;
|
||||
|
||||
// solium-disable-next-line security/no-inline-assembly
|
||||
assembly {
|
||||
success := staticcall(sub(gas(), 2000), 6, input, 0xc0, r, 0x60)
|
||||
// Use "invalid" to make gas estimation work
|
||||
switch success case 0 { invalid() }
|
||||
}
|
||||
|
||||
require(success, "pairing-add-failed");
|
||||
}
|
||||
|
||||
/*
|
||||
* @return r the product of a point on G1 and a scalar, i.e.
|
||||
* p == p.scalar_mul(1) and p.plus(p) == p.scalar_mul(2) for all
|
||||
* points p.
|
||||
*/
|
||||
function scalar_mul(G1Point memory p, uint256 s) internal view returns (G1Point memory r) {
|
||||
uint256[3] memory input;
|
||||
input[0] = p.X;
|
||||
input[1] = p.Y;
|
||||
input[2] = s;
|
||||
bool success;
|
||||
// solium-disable-next-line security/no-inline-assembly
|
||||
assembly {
|
||||
success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60)
|
||||
// Use "invalid" to make gas estimation work
|
||||
switch success case 0 { invalid() }
|
||||
}
|
||||
require(success, "pairing-mul-failed");
|
||||
}
|
||||
|
||||
/* @return The result of computing the pairing check
|
||||
* e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1
|
||||
* For example,
|
||||
* pairing([P1(), P1().negate()], [P2(), P2()]) should return true.
|
||||
*/
|
||||
function pairing(
|
||||
G1Point memory a1,
|
||||
G2Point memory a2,
|
||||
G1Point memory b1,
|
||||
G2Point memory b2,
|
||||
G1Point memory c1,
|
||||
G2Point memory c2,
|
||||
G1Point memory d1,
|
||||
G2Point memory d2
|
||||
) internal view returns (bool) {
|
||||
G1Point[4] memory p1 = [a1, b1, c1, d1];
|
||||
G2Point[4] memory p2 = [a2, b2, c2, d2];
|
||||
|
||||
uint256 inputSize = 24;
|
||||
uint256[] memory input = new uint256[](inputSize);
|
||||
|
||||
for (uint256 i = 0; i < 4; i++) {
|
||||
uint256 j = i * 6;
|
||||
input[j + 0] = p1[i].X;
|
||||
input[j + 1] = p1[i].Y;
|
||||
input[j + 2] = p2[i].X[0];
|
||||
input[j + 3] = p2[i].X[1];
|
||||
input[j + 4] = p2[i].Y[0];
|
||||
input[j + 5] = p2[i].Y[1];
|
||||
}
|
||||
|
||||
uint256[1] memory out;
|
||||
bool success;
|
||||
|
||||
// solium-disable-next-line security/no-inline-assembly
|
||||
assembly {
|
||||
success := staticcall(sub(gas(), 2000), 8, add(input, 0x20), mul(inputSize, 0x20), out, 0x20)
|
||||
// Use "invalid" to make gas estimation work
|
||||
switch success case 0 { invalid() }
|
||||
}
|
||||
|
||||
require(success, "pairing-opcode-failed");
|
||||
|
||||
return out[0] != 0;
|
||||
}
|
||||
}
|
||||
|
||||
contract Verifier {
|
||||
uint256 constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
|
||||
uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
|
||||
using Pairing for *;
|
||||
|
||||
struct VerifyingKey {
|
||||
Pairing.G1Point alfa1;
|
||||
Pairing.G2Point beta2;
|
||||
Pairing.G2Point gamma2;
|
||||
Pairing.G2Point delta2;
|
||||
Pairing.G1Point[7] IC;
|
||||
}
|
||||
|
||||
struct Proof {
|
||||
Pairing.G1Point A;
|
||||
Pairing.G2Point B;
|
||||
Pairing.G1Point C;
|
||||
}
|
||||
|
||||
function verifyingKey() internal pure returns (VerifyingKey memory vk) {
|
||||
vk.alfa1 = Pairing.G1Point(uint256(20692898189092739278193869274495556617788530808486270118371701516666252877969), uint256(11713062878292653967971378194351968039596396853904572879488166084231740557279));
|
||||
vk.beta2 = Pairing.G2Point([uint256(12168528810181263706895252315640534818222943348193302139358377162645029937006), uint256(281120578337195720357474965979947690431622127986816839208576358024608803542)], [uint256(16129176515713072042442734839012966563817890688785805090011011570989315559913), uint256(9011703453772030375124466642203641636825223906145908770308724549646909480510)]);
|
||||
vk.gamma2 = Pairing.G2Point([uint256(11559732032986387107991004021392285783925812861821192530917403151452391805634), uint256(10857046999023057135944570762232829481370756359578518086990519993285655852781)], [uint256(4082367875863433681332203403145435568316851327593401208105741076214120093531), uint256(8495653923123431417604973247489272438418190587263600148770280649306958101930)]);
|
||||
vk.delta2 = Pairing.G2Point([uint256(21280594949518992153305586783242820682644996932183186320680800072133486887432), uint256(150879136433974552800030963899771162647715069685890547489132178314736470662)], [uint256(1081836006956609894549771334721413187913047383331561601606260283167615953295), uint256(11434086686358152335540554643130007307617078324975981257823476472104616196090)]);
|
||||
vk.IC[0] = Pairing.G1Point(uint256(16225148364316337376768119297456868908427925829817748684139175309620217098814), uint256(5167268689450204162046084442581051565997733233062478317813755636162413164690));
|
||||
vk.IC[1] = Pairing.G1Point(uint256(12882377842072682264979317445365303375159828272423495088911985689463022094260), uint256(19488215856665173565526758360510125932214252767275816329232454875804474844786));
|
||||
vk.IC[2] = Pairing.G1Point(uint256(13083492661683431044045992285476184182144099829507350352128615182516530014777), uint256(602051281796153692392523702676782023472744522032670801091617246498551238913));
|
||||
vk.IC[3] = Pairing.G1Point(uint256(9732465972180335629969421513785602934706096902316483580882842789662669212890), uint256(2776526698606888434074200384264824461688198384989521091253289776235602495678));
|
||||
vk.IC[4] = Pairing.G1Point(uint256(8586364274534577154894611080234048648883781955345622578531233113180532234842), uint256(21276134929883121123323359450658320820075698490666870487450985603988214349407));
|
||||
vk.IC[5] = Pairing.G1Point(uint256(4910628533171597675018724709631788948355422829499855033965018665300386637884), uint256(20532468890024084510431799098097081600480376127870299142189696620752500664302));
|
||||
vk.IC[6] = Pairing.G1Point(uint256(15335858102289947642505450692012116222827233918185150176888641903531542034017), uint256(5311597067667671581646709998171703828965875677637292315055030353779531404812));
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* @returns Whether the proof is valid given the hardcoded verifying key
|
||||
* above and the public inputs
|
||||
*/
|
||||
function verifyProof(
|
||||
bytes memory proof,
|
||||
uint256[6] memory input
|
||||
) public view returns (bool) {
|
||||
uint256[8] memory p = abi.decode(proof, (uint256[8]));
|
||||
|
||||
// Make sure that each element in the proof is less than the prime q
|
||||
for (uint8 i = 0; i < p.length; i++) {
|
||||
require(p[i] < PRIME_Q, "verifier-proof-element-gte-prime-q");
|
||||
}
|
||||
|
||||
Proof memory _proof;
|
||||
_proof.A = Pairing.G1Point(p[0], p[1]);
|
||||
_proof.B = Pairing.G2Point([p[2], p[3]], [p[4], p[5]]);
|
||||
_proof.C = Pairing.G1Point(p[6], p[7]);
|
||||
|
||||
VerifyingKey memory vk = verifyingKey();
|
||||
|
||||
// Compute the linear combination vk_x
|
||||
Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0);
|
||||
vk_x = Pairing.plus(vk_x, vk.IC[0]);
|
||||
|
||||
// Make sure that every input is less than the snark scalar field
|
||||
for (uint256 i = 0; i < input.length; i++) {
|
||||
require(input[i] < SNARK_SCALAR_FIELD, "verifier-gte-snark-scalar-field");
|
||||
vk_x = Pairing.plus(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i]));
|
||||
}
|
||||
|
||||
return Pairing.pairing(
|
||||
Pairing.negate(_proof.A),
|
||||
_proof.B,
|
||||
vk.alfa1,
|
||||
vk.beta2,
|
||||
vk_x,
|
||||
vk.gamma2,
|
||||
_proof.C,
|
||||
vk.delta2
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// https://tornado.cash
|
||||
/*
|
||||
* d888888P dP a88888b. dP
|
||||
* 88 88 d8' `88 88
|
||||
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
|
||||
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
|
||||
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
|
||||
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
|
||||
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
|
||||
*/
|
||||
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
import "./ERC20Tornado.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
|
||||
contract cTornado is ERC20Tornado {
|
||||
address public immutable governance = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
|
||||
IERC20 public immutable comp;
|
||||
|
||||
constructor(
|
||||
IERC20 _comp,
|
||||
IVerifier _verifier,
|
||||
IHasher _hasher,
|
||||
uint256 _denomination,
|
||||
uint32 _merkleTreeHeight,
|
||||
IERC20 _token
|
||||
) ERC20Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight, _token) {
|
||||
require(address(_comp) != address(0), "Invalid COMP token address");
|
||||
comp = _comp;
|
||||
}
|
||||
|
||||
/// @dev Moves earned yield of the COMP token to the tornado governance contract
|
||||
/// To make it work you might need to call `comptroller.claimComp(cPoolAddress)` first
|
||||
function claimComp() external {
|
||||
comp.transfer(governance, comp.balanceOf(address(this)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
const eth = true
|
||||
const poolSize = '1000000000000000000'
|
||||
const hasherAddress = '0x83584f83f26aF4eDDA9CBe8C730bc87C364b28fe'
|
||||
const verifierAddress = '0xce172ce1F20EC0B3728c9965470eaf994A03557A'
|
||||
const deployerAddress = '0xCEe71753C9820f063b38FDbE4cFDAf1d3D928A80'
|
||||
const deploySalt = '0x0000000000000000000000000000000000000000000000000000000047941987'
|
||||
const rpcUrl = 'https://mainnet.infura.io'
|
||||
|
||||
const Web3 = require('web3')
|
||||
const web3 = new Web3(rpcUrl)
|
||||
|
||||
const contractData = require('./build/contracts/' + (eth ? 'ETHTornado.json' : 'ERC20Tornado.json'))
|
||||
const contract = new web3.eth.Contract(contractData.abi)
|
||||
const bytes = contract
|
||||
.deploy({
|
||||
data: contractData.bytecode,
|
||||
arguments: [verifierAddress, hasherAddress, poolSize, 20],
|
||||
})
|
||||
.encodeABI()
|
||||
|
||||
console.log('Deploy bytecode', bytes)
|
||||
|
||||
const deployer = new web3.eth.Contract(require('./build/contracts/IDeployer.json').abi, deployerAddress)
|
||||
const receipt = deployer.methods.deploy(bytes, deploySalt)
|
||||
receipt.then(console.log).catch(console.log)
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Binary file not shown.
After Width: | Height: | Size: 205 KiB |
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
29
index.html
29
index.html
|
@ -1,16 +1,17 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Snark mixer test</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
Open dev console!<br>
|
||||
Make sure your Metamask is unlocked and connected to Kovan (or other network you've deployed your contract to)<br>
|
||||
<a href="#" onclick="deposit()">Deposit</a>
|
||||
<a href="#" onclick="withdraw()">Withdraw</a>
|
||||
</p>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Tornado test</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
Open dev console!<br />
|
||||
Make sure your Metamask is unlocked and connected to Kovan (or other network you've deployed your
|
||||
contract to)<br />
|
||||
<a href="#" onclick="deposit()">Deposit</a>
|
||||
<a href="#" onclick="withdraw()">Withdraw</a>
|
||||
</p>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,194 +0,0 @@
|
|||
const jsStorage = require('./Storage')
|
||||
const mimcHasher = require('./MiMC')
|
||||
|
||||
class MerkleTree {
|
||||
|
||||
constructor(n_levels, zero_value, defaultElements, prefix, storage, hasher) {
|
||||
this.prefix = prefix
|
||||
this.storage = storage || new jsStorage()
|
||||
this.hasher = hasher || new mimcHasher()
|
||||
this.n_levels = n_levels
|
||||
this.zero_values = []
|
||||
this.totalElements = 0
|
||||
|
||||
let current_zero_value = zero_value || 0
|
||||
this.zero_values.push(current_zero_value)
|
||||
for (let i = 0; i < n_levels; i++) {
|
||||
current_zero_value = this.hasher.hash(i, current_zero_value, current_zero_value)
|
||||
this.zero_values.push(
|
||||
current_zero_value.toString(),
|
||||
)
|
||||
}
|
||||
if (defaultElements) {
|
||||
let level = 0
|
||||
this.totalElements = defaultElements.length
|
||||
defaultElements.forEach((element, i) => {
|
||||
this.storage.put(MerkleTree.index_to_key(prefix, level, i), element)
|
||||
})
|
||||
level++
|
||||
let numberOfElementsInLevel = Math.ceil(defaultElements.length / 2)
|
||||
for (level; level <= this.n_levels; level++) {
|
||||
for(let i = 0; i < numberOfElementsInLevel; i++) {
|
||||
const leftKey = MerkleTree.index_to_key(prefix, level - 1, 2 * i)
|
||||
const rightKey = MerkleTree.index_to_key(prefix, level - 1, 2 * i + 1)
|
||||
|
||||
const left = this.storage.get(leftKey)
|
||||
const right = this.storage.get_or_element(rightKey, this.zero_values[level - 1])
|
||||
|
||||
const subRoot = this.hasher.hash(null, left, right)
|
||||
this.storage.put(MerkleTree.index_to_key(prefix, level, i), subRoot)
|
||||
}
|
||||
numberOfElementsInLevel = Math.ceil(numberOfElementsInLevel / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static index_to_key(prefix, level, index) {
|
||||
const key = `${prefix}_tree_${level}_${index}`
|
||||
return key
|
||||
}
|
||||
|
||||
async root() {
|
||||
let root = await this.storage.get_or_element(
|
||||
MerkleTree.index_to_key(this.prefix, this.n_levels, 0),
|
||||
this.zero_values[this.n_levels],
|
||||
)
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
async path(index) {
|
||||
class PathTraverser {
|
||||
constructor(prefix, storage, zero_values) {
|
||||
this.prefix = prefix
|
||||
this.storage = storage
|
||||
this.zero_values = zero_values
|
||||
this.path_elements = []
|
||||
this.path_index = []
|
||||
}
|
||||
|
||||
async handle_index(level, element_index, sibling_index) {
|
||||
const sibling = await this.storage.get_or_element(
|
||||
MerkleTree.index_to_key(this.prefix, level, sibling_index),
|
||||
this.zero_values[level],
|
||||
)
|
||||
this.path_elements.push(sibling)
|
||||
this.path_index.push(element_index % 2)
|
||||
}
|
||||
}
|
||||
let traverser = new PathTraverser(this.prefix, this.storage, this.zero_values)
|
||||
const root = await this.storage.get_or_element(
|
||||
MerkleTree.index_to_key(this.prefix, this.n_levels, 0),
|
||||
this.zero_values[this.n_levels],
|
||||
)
|
||||
|
||||
const element = await this.storage.get_or_element(
|
||||
MerkleTree.index_to_key(this.prefix, 0, index),
|
||||
this.zero_values[0],
|
||||
)
|
||||
|
||||
await this.traverse(index, traverser)
|
||||
return {
|
||||
root,
|
||||
path_elements: traverser.path_elements,
|
||||
path_index: traverser.path_index,
|
||||
element
|
||||
}
|
||||
}
|
||||
|
||||
async update(index, element, insert = false) {
|
||||
if (!insert && index >= this.totalElements) {
|
||||
throw Error('Use insert method for new elements.')
|
||||
} else if(insert && index < this.totalElements) {
|
||||
throw Error('Use update method for existing elements.')
|
||||
}
|
||||
try {
|
||||
class UpdateTraverser {
|
||||
constructor(prefix, storage, hasher, element, zero_values) {
|
||||
this.prefix = prefix
|
||||
this.current_element = element
|
||||
this.zero_values = zero_values
|
||||
this.storage = storage
|
||||
this.hasher = hasher
|
||||
this.key_values_to_put = []
|
||||
}
|
||||
|
||||
async handle_index(level, element_index, sibling_index) {
|
||||
if (level == 0) {
|
||||
this.original_element = await this.storage.get_or_element(
|
||||
MerkleTree.index_to_key(this.prefix, level, element_index),
|
||||
this.zero_values[level],
|
||||
)
|
||||
}
|
||||
const sibling = await this.storage.get_or_element(
|
||||
MerkleTree.index_to_key(this.prefix, level, sibling_index),
|
||||
this.zero_values[level],
|
||||
)
|
||||
let left, right
|
||||
if (element_index % 2 == 0) {
|
||||
left = this.current_element
|
||||
right = sibling
|
||||
} else {
|
||||
left = sibling
|
||||
right = this.current_element
|
||||
}
|
||||
|
||||
this.key_values_to_put.push({
|
||||
key: MerkleTree.index_to_key(this.prefix, level, element_index),
|
||||
value: this.current_element,
|
||||
})
|
||||
this.current_element = this.hasher.hash(level, left, right)
|
||||
}
|
||||
}
|
||||
let traverser = new UpdateTraverser(
|
||||
this.prefix,
|
||||
this.storage,
|
||||
this.hasher,
|
||||
element,
|
||||
this.zero_values
|
||||
)
|
||||
|
||||
await this.traverse(index, traverser)
|
||||
traverser.key_values_to_put.push({
|
||||
key: MerkleTree.index_to_key(this.prefix, this.n_levels, 0),
|
||||
value: traverser.current_element,
|
||||
})
|
||||
|
||||
await this.storage.put_batch(traverser.key_values_to_put)
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async insert(element) {
|
||||
const index = this.totalElements
|
||||
await this.update(index, element, true)
|
||||
this.totalElements++
|
||||
}
|
||||
|
||||
async traverse(index, handler) {
|
||||
let current_index = index
|
||||
for (let i = 0; i < this.n_levels; i++) {
|
||||
let sibling_index = current_index
|
||||
if (current_index % 2 == 0) {
|
||||
sibling_index += 1
|
||||
} else {
|
||||
sibling_index -= 1
|
||||
}
|
||||
await handler.handle_index(i, current_index, sibling_index)
|
||||
current_index = Math.floor(current_index / 2)
|
||||
}
|
||||
}
|
||||
|
||||
getIndexByElement(element) {
|
||||
for(let i = this.totalElements - 1; i >= 0; i--) {
|
||||
const elementFromTree = this.storage.get(MerkleTree.index_to_key(this.prefix, 0, i))
|
||||
if (elementFromTree === element) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MerkleTree
|
13
lib/MiMC.js
13
lib/MiMC.js
|
@ -1,13 +0,0 @@
|
|||
const circomlib = require('circomlib')
|
||||
const mimcsponge = circomlib.mimcsponge
|
||||
const snarkjs = require('snarkjs')
|
||||
|
||||
const bigInt = snarkjs.bigInt
|
||||
|
||||
class MimcSpongeHasher {
|
||||
hash(level, left, right) {
|
||||
return mimcsponge.multiHash([bigInt(left), bigInt(right)]).toString()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MimcSpongeHasher
|
|
@ -1,39 +0,0 @@
|
|||
|
||||
|
||||
class JsStorage {
|
||||
constructor() {
|
||||
this.db = {}
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.db[key]
|
||||
}
|
||||
|
||||
get_or_element(key, defaultElement) {
|
||||
const element = this.db[key]
|
||||
if (element === undefined) {
|
||||
return defaultElement
|
||||
} else {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
put(key, value) {
|
||||
if (key === undefined || value === undefined) {
|
||||
throw Error('key or value is undefined')
|
||||
}
|
||||
this.db[key] = value
|
||||
}
|
||||
|
||||
del(key) {
|
||||
delete this.db[key]
|
||||
}
|
||||
|
||||
put_batch(key_values) {
|
||||
key_values.forEach(element => {
|
||||
this.db[element.key] = element.value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JsStorage
|
|
@ -1,9 +0,0 @@
|
|||
/* global artifacts */
|
||||
const Migrations = artifacts.require('Migrations')
|
||||
|
||||
module.exports = function(deployer) {
|
||||
if(deployer.network === 'mainnet') {
|
||||
return
|
||||
}
|
||||
deployer.deploy(Migrations)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
/* global artifacts */
|
||||
const Hasher = artifacts.require('Hasher')
|
||||
|
||||
module.exports = async function (deployer) {
|
||||
await deployer.deploy(Hasher)
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/* global artifacts */
|
||||
const path = require('path')
|
||||
|
||||
const mimcGenContract = require('circomlib/src/mimcsponge_gencontract.js')
|
||||
const Artifactor = require('truffle-artifactor')
|
||||
|
||||
const SEED = 'mimcsponge'
|
||||
|
||||
|
||||
module.exports = function(deployer) {
|
||||
return deployer.then( async () => {
|
||||
const contractsDir = path.join(__dirname, '..', 'build/contracts')
|
||||
let artifactor = new Artifactor(contractsDir)
|
||||
let mimcContractName = 'MiMC'
|
||||
await artifactor.save({
|
||||
contractName: mimcContractName,
|
||||
abi: mimcGenContract.abi,
|
||||
unlinked_binary: mimcGenContract.createCode(SEED, 220),
|
||||
}).then(async () => {
|
||||
const MiMC = artifacts.require(mimcContractName)
|
||||
await deployer.deploy(MiMC)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/* global artifacts */
|
||||
const Verifier = artifacts.require('Verifier')
|
||||
|
||||
module.exports = function(deployer) {
|
||||
module.exports = function (deployer) {
|
||||
deployer.deploy(Verifier)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/* global artifacts */
|
||||
require('dotenv').config({ path: '../.env' })
|
||||
const ETHTornado = artifacts.require('ETHTornado')
|
||||
const Verifier = artifacts.require('Verifier')
|
||||
const Hasher = artifacts.require('Hasher')
|
||||
|
||||
module.exports = function (deployer) {
|
||||
return deployer.then(async () => {
|
||||
const { MERKLE_TREE_HEIGHT, ETH_AMOUNT } = process.env
|
||||
const verifier = await Verifier.deployed()
|
||||
const hasher = await Hasher.deployed()
|
||||
const tornado = await deployer.deploy(
|
||||
ETHTornado,
|
||||
verifier.address,
|
||||
hasher.address,
|
||||
ETH_AMOUNT,
|
||||
MERKLE_TREE_HEIGHT,
|
||||
)
|
||||
console.log('ETHTornado address', tornado.address)
|
||||
})
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
/* global artifacts */
|
||||
require('dotenv').config({ path: '../.env' })
|
||||
const Mixer = artifacts.require('Mixer')
|
||||
const Verifier = artifacts.require('Verifier')
|
||||
const MiMC = artifacts.require('MiMC')
|
||||
|
||||
|
||||
module.exports = function(deployer, network, accounts) {
|
||||
return deployer.then(async () => {
|
||||
const { MERKLE_TREE_HEIGHT, AMOUNT, EMPTY_ELEMENT } = process.env
|
||||
const verifier = await Verifier.deployed()
|
||||
const miMC = await MiMC.deployed()
|
||||
await Mixer.link(MiMC, miMC.address)
|
||||
const mixer = await deployer.deploy(Mixer, verifier.address, AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, accounts[0])
|
||||
console.log('Mixer\'s address ', mixer.address)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* global artifacts */
|
||||
require('dotenv').config({ path: '../.env' })
|
||||
const ERC20Tornado = artifacts.require('ERC20Tornado')
|
||||
const Verifier = artifacts.require('Verifier')
|
||||
const Hasher = artifacts.require('Hasher')
|
||||
const ERC20Mock = artifacts.require('ERC20Mock')
|
||||
|
||||
module.exports = function (deployer) {
|
||||
return deployer.then(async () => {
|
||||
const { MERKLE_TREE_HEIGHT, ERC20_TOKEN, TOKEN_AMOUNT } = process.env
|
||||
const verifier = await Verifier.deployed()
|
||||
const hasher = await Hasher.deployed()
|
||||
let token = ERC20_TOKEN
|
||||
if (token === '') {
|
||||
const tokenInstance = await deployer.deploy(ERC20Mock)
|
||||
token = tokenInstance.address
|
||||
}
|
||||
const tornado = await deployer.deploy(
|
||||
ERC20Tornado,
|
||||
verifier.address,
|
||||
hasher.address,
|
||||
TOKEN_AMOUNT,
|
||||
MERKLE_TREE_HEIGHT,
|
||||
token,
|
||||
)
|
||||
console.log('ERC20Tornado address', tornado.address)
|
||||
})
|
||||
}
|
File diff suppressed because it is too large
Load Diff
62
package.json
62
package.json
|
@ -10,37 +10,55 @@
|
|||
"build:circuit:contract": "npx snarkjs generateverifier -v build/circuits/Verifier.sol --vk build/circuits/withdraw_verification_key.json",
|
||||
"build:circuit": "mkdir -p build/circuits && npm run build:circuit:compile && npm run build:circuit:setup && npm run build:circuit:bin && npm run build:circuit:contract",
|
||||
"build:contract": "npx truffle compile",
|
||||
"build:browserify": "npx browserify src/cli.js -o index.js --exclude worker_threads",
|
||||
"build": "npm run build:circuit && npm run build:contract && npm run build:browserify",
|
||||
"browserify": "npm run build:browserify",
|
||||
"test": "npx truffle test",
|
||||
"migrate": "npx truffle migrate --network kovan --reset",
|
||||
"migrate:mainnet": "npx truffle migrate --network mainnet",
|
||||
"migrate": "npm run migrate:kovan",
|
||||
"migrate:dev": "npx truffle migrate --network development --reset",
|
||||
"browserify": "npx browserify cli.js -o index.js --exclude worker_threads",
|
||||
"eslint": "npx eslint --ignore-path .gitignore .",
|
||||
"flat": "truffle-flattener contracts/Mixer.sol > Mixer_flat.sol"
|
||||
"migrate:kovan": "npx truffle migrate --network kovan --reset",
|
||||
"migrate:rinkeby": "npx truffle migrate --network rinkeby --reset",
|
||||
"migrate:mainnet": "npx truffle migrate --network mainnet",
|
||||
"eslint": "eslint --ext .js --ignore-path .gitignore .",
|
||||
"prettier:check": "prettier --check . --config .prettierrc",
|
||||
"prettier:fix": "prettier --write . --config .prettierrc",
|
||||
"lint": "yarn eslint && yarn prettier:check",
|
||||
"flat": "npx truffle-flattener contracts/ETHTornado.sol > ETHTornado_flat.sol && npx truffle-flattener contracts/ERC20Tornado.sol > ERC20Tornado_flat.sol",
|
||||
"download": "node scripts/downloadKeys.js",
|
||||
"coverage": "yarn truffle run coverage"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "^3.4.1",
|
||||
"@truffle/contract": "^4.0.39",
|
||||
"@truffle/hdwallet-provider": "^1.0.24",
|
||||
"axios": "^0.19.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bn-chai": "^1.0.1",
|
||||
"browserify": "^16.3.0",
|
||||
"browserify": "^16.5.0",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"circom": "0.0.30",
|
||||
"circomlib": "^0.0.10",
|
||||
"dotenv": "^8.0.0",
|
||||
"eslint": "^6.0.1",
|
||||
"ganache-cli": "^6.4.5",
|
||||
"snarkjs": "^0.1.16",
|
||||
"truffle": "^5.0.27",
|
||||
"truffle-artifactor": "^4.0.23",
|
||||
"truffle-contract": "^4.0.24",
|
||||
"truffle-hdwallet-provider": "^1.0.14",
|
||||
"web3": "^1.0.0-beta.55",
|
||||
"web3-utils": "^1.0.0-beta.55",
|
||||
"websnark": "git+https://github.com/peppersec/websnark.git#ed6a4d8a6fb081a62af26820980046bbb602d559"
|
||||
},
|
||||
"devDependencies": {
|
||||
"truffle-flattener": "^1.4.0"
|
||||
"circom": "^0.0.35",
|
||||
"circomlib": "git+https://github.com/tornadocash/circomlib.git#c372f14d324d57339c88451834bf2824e73bbdbc",
|
||||
"commander": "^4.1.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"eslint": "^7.19.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eth-json-rpc-filters": "^4.1.1",
|
||||
"fixed-merkle-tree": "^0.6.0",
|
||||
"ganache-cli": "^6.7.0",
|
||||
"prettier": "^2.2.1",
|
||||
"prettier-plugin-solidity": "^1.0.0-beta.3",
|
||||
"snarkjs": "git+https://github.com/tornadocash/snarkjs.git#869181cfaf7526fe8972073d31655493a04326d5",
|
||||
"solhint-plugin-prettier": "^0.0.5",
|
||||
"truffle": "^5.1.67",
|
||||
"truffle-flattener": "^1.4.2",
|
||||
"web3": "^1.3.4",
|
||||
"web3-utils": "^1.3.4",
|
||||
"websnark": "git+https://github.com/tornadocash/websnark.git#4c0af6a8b65aabea3c09f377f63c44e7a58afa6d",
|
||||
"solidity-coverage": "^0.7.20"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
// Generates Hasher artifact at compile-time using Truffle's external compiler
|
||||
// mechanism
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const genContract = require('circomlib/src/mimcsponge_gencontract.js')
|
||||
|
||||
// where Truffle will expect to find the results of the external compiler
|
||||
// command
|
||||
const outputPath = path.join(__dirname, '..', 'build', 'Hasher.json')
|
||||
|
||||
function main() {
|
||||
const contract = {
|
||||
contractName: 'Hasher',
|
||||
abi: genContract.abi,
|
||||
bytecode: genContract.createCode('mimcsponge', 220),
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(contract))
|
||||
}
|
||||
|
||||
main()
|
|
@ -0,0 +1,43 @@
|
|||
const axios = require('axios')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const files = ['withdraw.json', 'withdraw_proving_key.bin', 'Verifier.sol', 'withdraw_verification_key.json']
|
||||
const circuitsPath = __dirname + '/../build/circuits'
|
||||
const contractsPath = __dirname + '/../build/contracts'
|
||||
|
||||
async function downloadFile({ url, path }) {
|
||||
const writer = fs.createWriteStream(path)
|
||||
|
||||
const response = await axios({
|
||||
url,
|
||||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
})
|
||||
|
||||
response.data.pipe(writer)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', resolve)
|
||||
writer.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const release = await axios.get('https://api.github.com/repos/tornadocash/tornado-core/releases/latest')
|
||||
const { assets } = release.data
|
||||
if (!fs.existsSync(circuitsPath)) {
|
||||
fs.mkdirSync(circuitsPath, { recursive: true })
|
||||
fs.mkdirSync(contractsPath, { recursive: true })
|
||||
}
|
||||
for (let asset of assets) {
|
||||
if (files.includes(asset.name)) {
|
||||
console.log(`Downloading ${asset.name} ...`)
|
||||
await downloadFile({
|
||||
url: asset.browser_download_url,
|
||||
path: path.resolve(__dirname, circuitsPath, asset.name),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
|
@ -6,7 +6,7 @@ function send(method, params = []) {
|
|||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method,
|
||||
params
|
||||
params,
|
||||
}, (err, res) => {
|
||||
return err ? reject(err) : resolve(res)
|
||||
})
|
||||
|
@ -17,6 +17,10 @@ const takeSnapshot = async () => {
|
|||
return await send('evm_snapshot')
|
||||
}
|
||||
|
||||
const traceTransaction = async (tx) => {
|
||||
return await send('debug_traceTransaction', [tx, {}])
|
||||
}
|
||||
|
||||
const revertSnapshot = async (id) => {
|
||||
await send('evm_revert', [id])
|
||||
}
|
||||
|
@ -44,4 +48,5 @@ module.exports = {
|
|||
minerStop,
|
||||
minerStart,
|
||||
increaseTime,
|
||||
traceTransaction,
|
||||
}
|
|
@ -0,0 +1,640 @@
|
|||
#!/usr/bin/env node
|
||||
// Temporary demo client
|
||||
// Works both in browser and node.js
|
||||
|
||||
require('dotenv').config()
|
||||
const fs = require('fs')
|
||||
const axios = require('axios')
|
||||
const assert = require('assert')
|
||||
const snarkjs = require('snarkjs')
|
||||
const crypto = require('crypto')
|
||||
const circomlib = require('circomlib')
|
||||
const bigInt = snarkjs.bigInt
|
||||
const merkleTree = require('fixed-merkle-tree')
|
||||
const Web3 = require('web3')
|
||||
const buildGroth16 = require('websnark/src/groth16')
|
||||
const websnarkUtils = require('websnark/src/utils')
|
||||
const { toWei, fromWei, toBN, BN } = require('web3-utils')
|
||||
const config = require('./config')
|
||||
const program = require('commander')
|
||||
|
||||
let web3, tornado, circuit, proving_key, groth16, erc20, senderAccount, netId
|
||||
let MERKLE_TREE_HEIGHT, ETH_AMOUNT, TOKEN_AMOUNT, PRIVATE_KEY
|
||||
|
||||
/** Whether we are in a browser or node.js */
|
||||
const inBrowser = (typeof window !== 'undefined')
|
||||
let isLocalRPC = false
|
||||
|
||||
/** Generate random number of specified byte length */
|
||||
const rbigint = nbytes => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
|
||||
|
||||
/** Compute pedersen hash */
|
||||
const pedersenHash = data => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
|
||||
|
||||
/** BigNumber to hex string of specified length */
|
||||
function toHex(number, length = 32) {
|
||||
const str = number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)
|
||||
return '0x' + str.padStart(length * 2, '0')
|
||||
}
|
||||
|
||||
/** Display ETH account balance */
|
||||
async function printETHBalance({ address, name }) {
|
||||
console.log(`${name} ETH balance is`, web3.utils.fromWei(await web3.eth.getBalance(address)))
|
||||
}
|
||||
|
||||
/** Display ERC20 account balance */
|
||||
async function printERC20Balance({ address, name, tokenAddress }) {
|
||||
const erc20ContractJson = require(__dirname + '/../build/contracts/ERC20Mock.json')
|
||||
erc20 = tokenAddress ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : erc20
|
||||
console.log(`${name} Token Balance is`, web3.utils.fromWei(await erc20.methods.balanceOf(address).call()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create deposit object from secret and nullifier
|
||||
*/
|
||||
function createDeposit({ nullifier, secret }) {
|
||||
const deposit = { nullifier, secret }
|
||||
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
|
||||
deposit.commitment = pedersenHash(deposit.preimage)
|
||||
deposit.commitmentHex = toHex(deposit.commitment)
|
||||
deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
|
||||
deposit.nullifierHex = toHex(deposit.nullifierHash)
|
||||
return deposit
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a deposit
|
||||
* @param currency Сurrency
|
||||
* @param amount Deposit amount
|
||||
*/
|
||||
async function deposit({ currency, amount }) {
|
||||
const deposit = createDeposit({ nullifier: rbigint(31), secret: rbigint(31) })
|
||||
const note = toHex(deposit.preimage, 62)
|
||||
const noteString = `tornado-${currency}-${amount}-${netId}-${note}`
|
||||
console.log(`Your note: ${noteString}`)
|
||||
if (currency === 'eth') {
|
||||
await printETHBalance({ address: tornado._address, name: 'Tornado' })
|
||||
await printETHBalance({ address: senderAccount, name: 'Sender account' })
|
||||
const value = isLocalRPC ? ETH_AMOUNT : fromDecimals({ amount, decimals: 18 })
|
||||
console.log('Submitting deposit transaction')
|
||||
await tornado.methods.deposit(toHex(deposit.commitment)).send({ value, from: senderAccount, gas: 2e6 })
|
||||
await printETHBalance({ address: tornado._address, name: 'Tornado' })
|
||||
await printETHBalance({ address: senderAccount, name: 'Sender account' })
|
||||
} else { // a token
|
||||
await printERC20Balance({ address: tornado._address, name: 'Tornado' })
|
||||
await printERC20Balance({ address: senderAccount, name: 'Sender account' })
|
||||
const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
|
||||
const tokenAmount = isLocalRPC ? TOKEN_AMOUNT : fromDecimals({ amount, decimals })
|
||||
if (isLocalRPC) {
|
||||
console.log('Minting some test tokens to deposit')
|
||||
await erc20.methods.mint(senderAccount, tokenAmount).send({ from: senderAccount, gas: 2e6 })
|
||||
}
|
||||
|
||||
const allowance = await erc20.methods.allowance(senderAccount, tornado._address).call({ from: senderAccount })
|
||||
console.log('Current allowance is', fromWei(allowance))
|
||||
if (toBN(allowance).lt(toBN(tokenAmount))) {
|
||||
console.log('Approving tokens for deposit')
|
||||
await erc20.methods.approve(tornado._address, tokenAmount).send({ from: senderAccount, gas: 1e6 })
|
||||
}
|
||||
|
||||
console.log('Submitting deposit transaction')
|
||||
await tornado.methods.deposit(toHex(deposit.commitment)).send({ from: senderAccount, gas: 2e6 })
|
||||
await printERC20Balance({ address: tornado._address, name: 'Tornado' })
|
||||
await printERC20Balance({ address: senderAccount, name: 'Sender account' })
|
||||
}
|
||||
|
||||
return noteString
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate merkle tree for a deposit.
|
||||
* Download deposit events from the tornado, reconstructs merkle tree, finds our deposit leaf
|
||||
* in it and generates merkle proof
|
||||
* @param deposit Deposit object
|
||||
*/
|
||||
async function generateMerkleProof(deposit) {
|
||||
// Get all deposit events from smart contract and assemble merkle tree from them
|
||||
console.log('Getting current state from tornado contract')
|
||||
const events = await tornado.getPastEvents('Deposit', { fromBlock: 0, toBlock: 'latest' })
|
||||
const leaves = events
|
||||
.sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order
|
||||
.map(e => e.returnValues.commitment)
|
||||
const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves)
|
||||
|
||||
// Find current commitment in the tree
|
||||
const depositEvent = events.find(e => e.returnValues.commitment === toHex(deposit.commitment))
|
||||
const leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
|
||||
|
||||
// Validate that our data is correct
|
||||
const root = tree.root()
|
||||
const isValidRoot = await tornado.methods.isKnownRoot(toHex(root)).call()
|
||||
const isSpent = await tornado.methods.isSpent(toHex(deposit.nullifierHash)).call()
|
||||
assert(isValidRoot === true, 'Merkle tree is corrupted')
|
||||
assert(isSpent === false, 'The note is already spent')
|
||||
assert(leafIndex >= 0, 'The deposit is not found in the tree')
|
||||
|
||||
// Compute merkle proof of our commitment
|
||||
const { pathElements, pathIndices } = tree.path(leafIndex)
|
||||
return { pathElements, pathIndices, root: tree.root() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SNARK proof for withdrawal
|
||||
* @param deposit Deposit object
|
||||
* @param recipient Funds recipient
|
||||
* @param relayer Relayer address
|
||||
* @param fee Relayer fee
|
||||
* @param refund Receive ether for exchanged tokens
|
||||
*/
|
||||
async function generateProof({ deposit, recipient, relayerAddress = 0, fee = 0, refund = 0 }) {
|
||||
// Compute merkle proof of our commitment
|
||||
const { root, pathElements, pathIndices } = await generateMerkleProof(deposit)
|
||||
|
||||
// Prepare circuit input
|
||||
const input = {
|
||||
// Public snark inputs
|
||||
root: root,
|
||||
nullifierHash: deposit.nullifierHash,
|
||||
recipient: bigInt(recipient),
|
||||
relayer: bigInt(relayerAddress),
|
||||
fee: bigInt(fee),
|
||||
refund: bigInt(refund),
|
||||
|
||||
// Private snark inputs
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
}
|
||||
|
||||
console.log('Generating SNARK proof')
|
||||
console.time('Proof time')
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
console.timeEnd('Proof time')
|
||||
|
||||
const args = [
|
||||
toHex(input.root),
|
||||
toHex(input.nullifierHash),
|
||||
toHex(input.recipient, 20),
|
||||
toHex(input.relayer, 20),
|
||||
toHex(input.fee),
|
||||
toHex(input.refund),
|
||||
]
|
||||
|
||||
return { proof, args }
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an ETH withdrawal
|
||||
* @param noteString Note to withdraw
|
||||
* @param recipient Recipient address
|
||||
*/
|
||||
async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund = '0' }) {
|
||||
if (currency === 'eth' && refund !== '0') {
|
||||
throw new Error('The ETH purchase is supposted to be 0 for ETH withdrawals')
|
||||
}
|
||||
refund = toWei(refund)
|
||||
if (relayerURL) {
|
||||
if (relayerURL.endsWith('.eth')) {
|
||||
throw new Error('ENS name resolving is not supported. Please provide DNS name of the relayer. See instuctions in README.md')
|
||||
}
|
||||
const relayerStatus = await axios.get(relayerURL + '/status')
|
||||
const { relayerAddress, netId, gasPrices, ethPrices, relayerServiceFee } = relayerStatus.data
|
||||
assert(netId === await web3.eth.net.getId() || netId === '*', 'This relay is for different network')
|
||||
console.log('Relay address: ', relayerAddress)
|
||||
|
||||
const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
|
||||
const fee = calculateFee({ gasPrices, currency, amount, refund, ethPrices, relayerServiceFee, decimals })
|
||||
if (fee.gt(fromDecimals({ amount, decimals }))) {
|
||||
throw new Error('Too high refund')
|
||||
}
|
||||
const { proof, args } = await generateProof({ deposit, recipient, relayerAddress, fee, refund })
|
||||
|
||||
console.log('Sending withdraw transaction through relay')
|
||||
try {
|
||||
const relay = await axios.post(relayerURL + '/relay', { contract: tornado._address, proof, args })
|
||||
if (netId === 1 || netId === 42) {
|
||||
console.log(`Transaction submitted through the relay. View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${relay.data.txHash}`)
|
||||
} else {
|
||||
console.log(`Transaction submitted through the relay. The transaction hash is ${relay.data.txHash}`)
|
||||
}
|
||||
|
||||
const receipt = await waitForTxReceipt({ txHash: relay.data.txHash })
|
||||
console.log('Transaction mined in block', receipt.blockNumber)
|
||||
} catch (e) {
|
||||
if (e.response) {
|
||||
console.error(e.response.data.error)
|
||||
} else {
|
||||
console.error(e.message)
|
||||
}
|
||||
}
|
||||
} else { // using private key
|
||||
const { proof, args } = await generateProof({ deposit, recipient, refund })
|
||||
|
||||
console.log('Submitting withdraw transaction')
|
||||
await tornado.methods.withdraw(proof, ...args).send({ from: senderAccount, value: refund.toString(), gas: 1e6 })
|
||||
.on('transactionHash', function (txHash) {
|
||||
if (netId === 1 || netId === 42) {
|
||||
console.log(`View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${txHash}`)
|
||||
} else {
|
||||
console.log(`The transaction hash is ${txHash}`)
|
||||
}
|
||||
}).on('error', function (e) {
|
||||
console.error('on transactionHash error', e.message)
|
||||
})
|
||||
}
|
||||
console.log('Done')
|
||||
}
|
||||
|
||||
function fromDecimals({ amount, decimals }) {
|
||||
amount = amount.toString()
|
||||
let ether = amount.toString()
|
||||
const base = new BN('10').pow(new BN(decimals))
|
||||
const baseLength = base.toString(10).length - 1 || 1
|
||||
|
||||
const negative = ether.substring(0, 1) === '-'
|
||||
if (negative) {
|
||||
ether = ether.substring(1)
|
||||
}
|
||||
|
||||
if (ether === '.') {
|
||||
throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, invalid value')
|
||||
}
|
||||
|
||||
// Split it into a whole and fractional part
|
||||
const comps = ether.split('.')
|
||||
if (comps.length > 2) {
|
||||
throw new Error(
|
||||
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal points',
|
||||
)
|
||||
}
|
||||
|
||||
let whole = comps[0]
|
||||
let fraction = comps[1]
|
||||
|
||||
if (!whole) {
|
||||
whole = '0'
|
||||
}
|
||||
if (!fraction) {
|
||||
fraction = '0'
|
||||
}
|
||||
if (fraction.length > baseLength) {
|
||||
throw new Error(
|
||||
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal places',
|
||||
)
|
||||
}
|
||||
|
||||
while (fraction.length < baseLength) {
|
||||
fraction += '0'
|
||||
}
|
||||
|
||||
whole = new BN(whole)
|
||||
fraction = new BN(fraction)
|
||||
let wei = whole.mul(base).add(fraction)
|
||||
|
||||
if (negative) {
|
||||
wei = wei.mul(negative)
|
||||
}
|
||||
|
||||
return new BN(wei.toString(10), 10)
|
||||
}
|
||||
|
||||
function toDecimals(value, decimals, fixed) {
|
||||
const zero = new BN(0)
|
||||
const negative1 = new BN(-1)
|
||||
decimals = decimals || 18
|
||||
fixed = fixed || 7
|
||||
|
||||
value = new BN(value)
|
||||
const negative = value.lt(zero)
|
||||
const base = new BN('10').pow(new BN(decimals))
|
||||
const baseLength = base.toString(10).length - 1 || 1
|
||||
|
||||
if (negative) {
|
||||
value = value.mul(negative1)
|
||||
}
|
||||
|
||||
let fraction = value.mod(base).toString(10)
|
||||
while (fraction.length < baseLength) {
|
||||
fraction = `0${fraction}`
|
||||
}
|
||||
fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1]
|
||||
|
||||
const whole = value.div(base).toString(10)
|
||||
value = `${whole}${fraction === '0' ? '' : `.${fraction}`}`
|
||||
|
||||
if (negative) {
|
||||
value = `-${value}`
|
||||
}
|
||||
|
||||
if (fixed) {
|
||||
value = value.slice(0, fixed)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function getCurrentNetworkName() {
|
||||
switch (netId) {
|
||||
case 1:
|
||||
return ''
|
||||
case 42:
|
||||
return 'kovan.'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function calculateFee({ gasPrices, currency, amount, refund, ethPrices, relayerServiceFee, decimals }) {
|
||||
const decimalsPoint = Math.floor(relayerServiceFee) === Number(relayerServiceFee) ?
|
||||
0 :
|
||||
relayerServiceFee.toString().split('.')[1].length
|
||||
const roundDecimal = 10 ** decimalsPoint
|
||||
const total = toBN(fromDecimals({ amount, decimals }))
|
||||
const feePercent = total.mul(toBN(relayerServiceFee * roundDecimal)).div(toBN(roundDecimal * 100))
|
||||
const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(5e5))
|
||||
let desiredFee
|
||||
switch (currency) {
|
||||
case 'eth': {
|
||||
desiredFee = expense.add(feePercent)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
desiredFee = expense.add(toBN(refund))
|
||||
.mul(toBN(10 ** decimals))
|
||||
.div(toBN(ethPrices[currency]))
|
||||
desiredFee = desiredFee.add(feePercent)
|
||||
break
|
||||
}
|
||||
}
|
||||
return desiredFee
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for transaction to be mined
|
||||
* @param txHash Hash of transaction
|
||||
* @param attempts
|
||||
* @param delay
|
||||
*/
|
||||
function waitForTxReceipt({ txHash, attempts = 60, delay = 1000 }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkForTx = async (txHash, retryAttempt = 0) => {
|
||||
const result = await web3.eth.getTransactionReceipt(txHash)
|
||||
if (!result || !result.blockNumber) {
|
||||
if (retryAttempt <= attempts) {
|
||||
setTimeout(() => checkForTx(txHash, retryAttempt + 1), delay)
|
||||
} else {
|
||||
reject(new Error('tx was not mined'))
|
||||
}
|
||||
} else {
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
checkForTx(txHash)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Tornado.cash note
|
||||
* @param noteString the note
|
||||
*/
|
||||
function parseNote(noteString) {
|
||||
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g
|
||||
const match = noteRegex.exec(noteString)
|
||||
if (!match) {
|
||||
throw new Error('The note has invalid format')
|
||||
}
|
||||
|
||||
const buf = Buffer.from(match.groups.note, 'hex')
|
||||
const nullifier = bigInt.leBuff2int(buf.slice(0, 31))
|
||||
const secret = bigInt.leBuff2int(buf.slice(31, 62))
|
||||
const deposit = createDeposit({ nullifier, secret })
|
||||
const netId = Number(match.groups.netId)
|
||||
|
||||
return { currency: match.groups.currency, amount: match.groups.amount, netId, deposit }
|
||||
}
|
||||
|
||||
async function loadDepositData({ deposit }) {
|
||||
try {
|
||||
const eventWhenHappened = await tornado.getPastEvents('Deposit', {
|
||||
filter: {
|
||||
commitment: deposit.commitmentHex,
|
||||
},
|
||||
fromBlock: 0,
|
||||
toBlock: 'latest',
|
||||
})
|
||||
if (eventWhenHappened.length === 0) {
|
||||
throw new Error('There is no related deposit, the note is invalid')
|
||||
}
|
||||
|
||||
const { timestamp } = eventWhenHappened[0].returnValues
|
||||
const txHash = eventWhenHappened[0].transactionHash
|
||||
const isSpent = await tornado.methods.isSpent(deposit.nullifierHex).call()
|
||||
const receipt = await web3.eth.getTransactionReceipt(txHash)
|
||||
|
||||
return { timestamp, txHash, isSpent, from: receipt.from, commitment: deposit.commitmentHex }
|
||||
} catch (e) {
|
||||
console.error('loadDepositData', e)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
async function loadWithdrawalData({ amount, currency, deposit }) {
|
||||
try {
|
||||
const events = await tornado.getPastEvents('Withdrawal', {
|
||||
fromBlock: 0,
|
||||
toBlock: 'latest',
|
||||
})
|
||||
|
||||
const withdrawEvent = events.filter((event) => {
|
||||
return event.returnValues.nullifierHash === deposit.nullifierHex
|
||||
})[0]
|
||||
|
||||
const fee = withdrawEvent.returnValues.fee
|
||||
const decimals = config.deployments[`netId${netId}`][currency].decimals
|
||||
const withdrawalAmount = toBN(fromDecimals({ amount, decimals })).sub(
|
||||
toBN(fee),
|
||||
)
|
||||
const { timestamp } = await web3.eth.getBlock(withdrawEvent.blockHash)
|
||||
return {
|
||||
amount: toDecimals(withdrawalAmount, decimals, 9),
|
||||
txHash: withdrawEvent.transactionHash,
|
||||
to: withdrawEvent.returnValues.to,
|
||||
timestamp,
|
||||
nullifier: deposit.nullifierHex,
|
||||
fee: toDecimals(fee, decimals, 9),
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('loadWithdrawalData', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init web3, contracts, and snark
|
||||
*/
|
||||
async function init({ rpc, noteNetId, currency = 'dai', amount = '100' }) {
|
||||
let contractJson, erc20ContractJson, erc20tornadoJson, tornadoAddress, tokenAddress
|
||||
// TODO do we need this? should it work in browser really?
|
||||
if (inBrowser) {
|
||||
// Initialize using injected web3 (Metamask)
|
||||
// To assemble web version run `npm run browserify`
|
||||
web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 })
|
||||
contractJson = await (await fetch('build/contracts/ETHTornado.json')).json()
|
||||
circuit = await (await fetch('build/circuits/withdraw.json')).json()
|
||||
proving_key = await (await fetch('build/circuits/withdraw_proving_key.bin')).arrayBuffer()
|
||||
MERKLE_TREE_HEIGHT = 20
|
||||
ETH_AMOUNT = 1e18
|
||||
TOKEN_AMOUNT = 1e19
|
||||
senderAccount = (await web3.eth.getAccounts())[0]
|
||||
} else {
|
||||
// Initialize from local node
|
||||
web3 = new Web3(rpc, null, { transactionConfirmationBlocks: 1 })
|
||||
contractJson = require(__dirname + '/../build/contracts/ETHTornado.json')
|
||||
circuit = require(__dirname + '/../build/circuits/withdraw.json')
|
||||
proving_key = fs.readFileSync(__dirname + '/../build/circuits/withdraw_proving_key.bin').buffer
|
||||
MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT || 20
|
||||
ETH_AMOUNT = process.env.ETH_AMOUNT
|
||||
TOKEN_AMOUNT = process.env.TOKEN_AMOUNT
|
||||
PRIVATE_KEY = process.env.PRIVATE_KEY
|
||||
if (PRIVATE_KEY) {
|
||||
const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY)
|
||||
web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY)
|
||||
web3.eth.defaultAccount = account.address
|
||||
senderAccount = account.address
|
||||
} else {
|
||||
console.log('Warning! PRIVATE_KEY not found. Please provide PRIVATE_KEY in .env file if you deposit')
|
||||
}
|
||||
erc20ContractJson = require(__dirname + '/../build/contracts/ERC20Mock.json')
|
||||
erc20tornadoJson = require(__dirname + '/../build/contracts/ERC20Tornado.json')
|
||||
}
|
||||
// groth16 initialises a lot of Promises that will never be resolved, that's why we need to use process.exit to terminate the CLI
|
||||
groth16 = await buildGroth16()
|
||||
netId = await web3.eth.net.getId()
|
||||
if (noteNetId && Number(noteNetId) !== netId) {
|
||||
throw new Error('This note is for a different network. Specify the --rpc option explicitly')
|
||||
}
|
||||
isLocalRPC = netId > 42
|
||||
|
||||
if (isLocalRPC) {
|
||||
tornadoAddress = currency === 'eth' ? contractJson.networks[netId].address : erc20tornadoJson.networks[netId].address
|
||||
tokenAddress = currency !== 'eth' ? erc20ContractJson.networks[netId].address : null
|
||||
senderAccount = (await web3.eth.getAccounts())[0]
|
||||
} else {
|
||||
try {
|
||||
tornadoAddress = config.deployments[`netId${netId}`][currency].instanceAddress[amount]
|
||||
if (!tornadoAddress) {
|
||||
throw new Error()
|
||||
}
|
||||
tokenAddress = config.deployments[`netId${netId}`][currency].tokenAddress
|
||||
} catch (e) {
|
||||
console.error('There is no such tornado instance, check the currency and amount you provide')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
tornado = new web3.eth.Contract(contractJson.abi, tornadoAddress)
|
||||
erc20 = currency !== 'eth' ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : {}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (inBrowser) {
|
||||
const instance = { currency: 'eth', amount: '0.1' }
|
||||
await init(instance)
|
||||
window.deposit = async () => {
|
||||
await deposit(instance)
|
||||
}
|
||||
window.withdraw = async () => {
|
||||
const noteString = prompt('Enter the note to withdraw')
|
||||
const recipient = (await web3.eth.getAccounts())[0]
|
||||
|
||||
const { currency, amount, netId, deposit } = parseNote(noteString)
|
||||
await init({ noteNetId: netId, currency, amount })
|
||||
await withdraw({ deposit, currency, amount, recipient })
|
||||
}
|
||||
} else {
|
||||
program
|
||||
.option('-r, --rpc <URL>', 'The RPC, CLI should interact with', 'http://localhost:8545')
|
||||
.option('-R, --relayer <URL>', 'Withdraw via relayer')
|
||||
program
|
||||
.command('deposit <currency> <amount>')
|
||||
.description('Submit a deposit of specified currency and amount from default eth account and return the resulting note. The currency is one of (ETH|DAI|cDAI|USDC|cUSDC|USDT). The amount depends on currency, see config.js file or visit https://tornado.cash.')
|
||||
.action(async (currency, amount) => {
|
||||
currency = currency.toLowerCase()
|
||||
await init({ rpc: program.rpc, currency, amount })
|
||||
await deposit({ currency, amount })
|
||||
})
|
||||
program
|
||||
.command('withdraw <note> <recipient> [ETH_purchase]')
|
||||
.description('Withdraw a note to a recipient account using relayer or specified private key. You can exchange some of your deposit`s tokens to ETH during the withdrawal by specifing ETH_purchase (e.g. 0.01) to pay for gas in future transactions. Also see the --relayer option.')
|
||||
.action(async (noteString, recipient, refund) => {
|
||||
const { currency, amount, netId, deposit } = parseNote(noteString)
|
||||
await init({ rpc: program.rpc, noteNetId: netId, currency, amount })
|
||||
await withdraw({ deposit, currency, amount, recipient, refund, relayerURL: program.relayer })
|
||||
})
|
||||
program
|
||||
.command('balance <address> [token_address]')
|
||||
.description('Check ETH and ERC20 balance')
|
||||
.action(async (address, tokenAddress) => {
|
||||
await init({ rpc: program.rpc })
|
||||
await printETHBalance({ address, name: '' })
|
||||
if (tokenAddress) {
|
||||
await printERC20Balance({ address, name: '', tokenAddress })
|
||||
}
|
||||
})
|
||||
program
|
||||
.command('compliance <note>')
|
||||
.description('Shows the deposit and withdrawal of the provided note. This might be necessary to show the origin of assets held in your withdrawal address.')
|
||||
.action(async (noteString) => {
|
||||
const { currency, amount, netId, deposit } = parseNote(noteString)
|
||||
await init({ rpc: program.rpc, noteNetId: netId, currency, amount })
|
||||
const depositInfo = await loadDepositData({ deposit })
|
||||
const depositDate = new Date(depositInfo.timestamp * 1000)
|
||||
console.log('\n=============Deposit=================')
|
||||
console.log('Deposit :', amount, currency)
|
||||
console.log('Date :', depositDate.toLocaleDateString(), depositDate.toLocaleTimeString())
|
||||
console.log('From :', `https://${getCurrentNetworkName()}etherscan.io/address/${depositInfo.from}`)
|
||||
console.log('Transaction :', `https://${getCurrentNetworkName()}etherscan.io/tx/${depositInfo.txHash}`)
|
||||
console.log('Commitment :', depositInfo.commitment)
|
||||
if (deposit.isSpent) {
|
||||
console.log('The note was not spent')
|
||||
}
|
||||
|
||||
const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit })
|
||||
const withdrawalDate = new Date(withdrawInfo.timestamp * 1000)
|
||||
console.log('\n=============Withdrawal==============')
|
||||
console.log('Withdrawal :', withdrawInfo.amount, currency)
|
||||
console.log('Relayer Fee :', withdrawInfo.fee, currency)
|
||||
console.log('Date :', withdrawalDate.toLocaleDateString(), withdrawalDate.toLocaleTimeString())
|
||||
console.log('To :', `https://${getCurrentNetworkName()}etherscan.io/address/${withdrawInfo.to}`)
|
||||
console.log('Transaction :', `https://${getCurrentNetworkName()}etherscan.io/tx/${withdrawInfo.txHash}`)
|
||||
console.log('Nullifier :', withdrawInfo.nullifier)
|
||||
})
|
||||
program
|
||||
.command('test')
|
||||
.description('Perform an automated test. It deposits and withdraws one ETH and one ERC20 note. Uses ganache.')
|
||||
.action(async () => {
|
||||
console.log('Start performing ETH deposit-withdraw test')
|
||||
let currency = 'eth'
|
||||
let amount = '0.1'
|
||||
await init({ rpc: program.rpc, currency, amount })
|
||||
let noteString = await deposit({ currency, amount })
|
||||
let parsedNote = parseNote(noteString)
|
||||
await withdraw({ deposit: parsedNote.deposit, currency, amount, recipient: senderAccount, relayerURL: program.relayer })
|
||||
|
||||
console.log('\nStart performing DAI deposit-withdraw test')
|
||||
currency = 'dai'
|
||||
amount = '100'
|
||||
await init({ rpc: program.rpc, currency, amount })
|
||||
noteString = await deposit({ currency, amount })
|
||||
; (parsedNote = parseNote(noteString))
|
||||
await withdraw({ deposit: parsedNote.deposit, currency, amount, recipient: senderAccount, refund: '0.02', relayerURL: program.relayer })
|
||||
})
|
||||
try {
|
||||
await program.parseAsync(process.argv)
|
||||
process.exit(0)
|
||||
} catch (e) {
|
||||
console.log('Error:', e)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
|
@ -0,0 +1,140 @@
|
|||
require('dotenv').config()
|
||||
|
||||
module.exports = {
|
||||
deployments: {
|
||||
netId1: {
|
||||
eth: {
|
||||
instanceAddress: {
|
||||
0.1: '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
|
||||
1: '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
|
||||
10: '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
|
||||
100: '0xA160cdAB225685dA1d56aa342Ad8841c3b53f291',
|
||||
},
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
dai: {
|
||||
instanceAddress: {
|
||||
100: '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
|
||||
1000: '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
|
||||
10000: '0xF60dD140cFf0706bAE9Cd734Ac3ae76AD9eBC32A',
|
||||
100000: undefined,
|
||||
},
|
||||
tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
|
||||
symbol: 'DAI',
|
||||
decimals: 18,
|
||||
},
|
||||
cdai: {
|
||||
instanceAddress: {
|
||||
5000: '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
|
||||
50000: '0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659',
|
||||
500000: '0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00',
|
||||
5000000: undefined,
|
||||
},
|
||||
tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643',
|
||||
symbol: 'cDAI',
|
||||
decimals: 8,
|
||||
},
|
||||
usdc: {
|
||||
instanceAddress: {
|
||||
100: '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
|
||||
1000: '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
|
||||
10000: '0xD691F27f38B395864Ea86CfC7253969B409c362d',
|
||||
100000: undefined,
|
||||
},
|
||||
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||
symbol: 'USDC',
|
||||
decimals: 6,
|
||||
},
|
||||
cusdc: {
|
||||
instanceAddress: {
|
||||
5000: '0xaEaaC358560e11f52454D997AAFF2c5731B6f8a6',
|
||||
50000: '0x1356c899D8C9467C7f71C195612F8A395aBf2f0a',
|
||||
500000: '0xA60C772958a3eD56c1F15dD055bA37AC8e523a0D',
|
||||
5000000: undefined,
|
||||
},
|
||||
tokenAddress: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
symbol: 'cUSDC',
|
||||
decimals: 8,
|
||||
},
|
||||
usdt: {
|
||||
instanceAddress: {
|
||||
100: '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
|
||||
1000: '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
|
||||
10000: '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a',
|
||||
100000: '0x9AD122c22B14202B4490eDAf288FDb3C7cb3ff5E',
|
||||
},
|
||||
tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
||||
symbol: 'USDT',
|
||||
decimals: 6,
|
||||
},
|
||||
},
|
||||
netId42: {
|
||||
eth: {
|
||||
instanceAddress: {
|
||||
0.1: '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
|
||||
1: '0xD6a6AC46d02253c938B96D12BE439F570227aE8E',
|
||||
10: '0xe1BE96331391E519471100c3c1528B66B8F4e5a7',
|
||||
100: '0xd037E0Ac98Dab2fCb7E296c69C6e52767Ae5414D',
|
||||
},
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
dai: {
|
||||
instanceAddress: {
|
||||
100: '0xdf2d3cC5F361CF95b3f62c4bB66deFe3FDE47e3D',
|
||||
1000: '0xD96291dFa35d180a71964D0894a1Ae54247C4ccD',
|
||||
10000: '0xb192794f72EA45e33C3DF6fe212B9c18f6F45AE3',
|
||||
100000: undefined,
|
||||
},
|
||||
tokenAddress: '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa',
|
||||
symbol: 'DAI',
|
||||
decimals: 18,
|
||||
},
|
||||
cdai: {
|
||||
instanceAddress: {
|
||||
5000: '0x6Fc9386ABAf83147b3a89C36D422c625F44121C8',
|
||||
50000: '0x7182EA067e0f050997444FCb065985Fd677C16b6',
|
||||
500000: '0xC22ceFd90fbd1FdEeE554AE6Cc671179BC3b10Ae',
|
||||
5000000: undefined,
|
||||
},
|
||||
tokenAddress: '0xe7bc397DBd069fC7d0109C0636d06888bb50668c',
|
||||
symbol: 'cDAI',
|
||||
decimals: 8,
|
||||
},
|
||||
usdc: {
|
||||
instanceAddress: {
|
||||
100: '0x137E2B6d185018e7f09f6cf175a970e7fC73826C',
|
||||
1000: '0xcC7f1633A5068E86E3830e692e3e3f8f520525Af',
|
||||
10000: '0x28C8f149a0ab8A9bdB006B8F984fFFCCE52ef5EF',
|
||||
100000: undefined,
|
||||
},
|
||||
tokenAddress: '0x75B0622Cec14130172EaE9Cf166B92E5C112FaFF',
|
||||
symbol: 'USDC',
|
||||
decimals: 6,
|
||||
},
|
||||
cusdc: {
|
||||
instanceAddress: {
|
||||
5000: '0xc0648F28ABA385c8a1421Bbf1B59e3c474F89cB0',
|
||||
50000: '0x0C53853379c6b1A7B74E0A324AcbDD5Eabd4981D',
|
||||
500000: '0xf84016A0E03917cBe700D318EB1b7a53e6e3dEe1',
|
||||
5000000: undefined,
|
||||
},
|
||||
tokenAddress: '0xcfC9bB230F00bFFDB560fCe2428b4E05F3442E35',
|
||||
symbol: 'cUSDC',
|
||||
decimals: 8,
|
||||
},
|
||||
usdt: {
|
||||
instanceAddress: {
|
||||
100: '0x327853Da7916a6A0935563FB1919A48843036b42',
|
||||
1000: '0x531AA4DF5858EA1d0031Dad16e3274609DE5AcC0',
|
||||
10000: '0x0958275F0362cf6f07D21373aEE0cf37dFe415dD',
|
||||
100000: '0x14aEd24B67EaF3FF28503eB92aeb217C47514364',
|
||||
},
|
||||
tokenAddress: '0x03c5F29e9296006876d8DF210BCFfD7EA5Db1Cf1',
|
||||
symbol: 'USDT',
|
||||
decimals: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
const fs = require('fs')
|
||||
const assert = require('assert')
|
||||
const { bigInt } = require('snarkjs')
|
||||
const crypto = require('crypto')
|
||||
const circomlib = require('circomlib')
|
||||
const merkleTree = require('fixed-merkle-tree')
|
||||
const Web3 = require('web3')
|
||||
const buildGroth16 = require('websnark/src/groth16')
|
||||
const websnarkUtils = require('websnark/src/utils')
|
||||
const { toWei } = require('web3-utils')
|
||||
|
||||
let web3, contract, netId, circuit, proving_key, groth16
|
||||
const MERKLE_TREE_HEIGHT = 20
|
||||
const RPC_URL = 'https://kovan.infura.io/v3/0279e3bdf3ee49d0b547c643c2ef78ef'
|
||||
const PRIVATE_KEY = 'ad5b6eb7ee88173fa43dedcff8b1d9024d03f6307a1143ecf04bea8ed40f283f' // 0x94462e71A887756704f0fb1c0905264d487972fE
|
||||
const CONTRACT_ADDRESS = '0xD6a6AC46d02253c938B96D12BE439F570227aE8E'
|
||||
const AMOUNT = '1'
|
||||
// CURRENCY = 'ETH'
|
||||
|
||||
/** Generate random number of specified byte length */
|
||||
const rbigint = (nbytes) => bigInt.leBuff2int(crypto.randomBytes(nbytes))
|
||||
|
||||
/** Compute pedersen hash */
|
||||
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
|
||||
|
||||
/** BigNumber to hex string of specified length */
|
||||
const toHex = (number, length = 32) =>
|
||||
'0x' +
|
||||
(number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)).padStart(length * 2, '0')
|
||||
|
||||
/**
|
||||
* Create deposit object from secret and nullifier
|
||||
*/
|
||||
function createDeposit(nullifier, secret) {
|
||||
let deposit = { nullifier, secret }
|
||||
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
|
||||
deposit.commitment = pedersenHash(deposit.preimage)
|
||||
deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
|
||||
return deposit
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an ETH deposit
|
||||
*/
|
||||
async function deposit() {
|
||||
const deposit = createDeposit(rbigint(31), rbigint(31))
|
||||
console.log('Sending deposit transaction...')
|
||||
const tx = await contract.methods
|
||||
.deposit(toHex(deposit.commitment))
|
||||
.send({ value: toWei(AMOUNT), from: web3.eth.defaultAccount, gas: 2e6 })
|
||||
console.log(`https://kovan.etherscan.io/tx/${tx.transactionHash}`)
|
||||
return `tornado-eth-${AMOUNT}-${netId}-${toHex(deposit.preimage, 62)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an ETH withdrawal
|
||||
* @param note Note to withdraw
|
||||
* @param recipient Recipient address
|
||||
*/
|
||||
async function withdraw(note, recipient) {
|
||||
const deposit = parseNote(note)
|
||||
const { proof, args } = await generateSnarkProof(deposit, recipient)
|
||||
console.log('Sending withdrawal transaction...')
|
||||
const tx = await contract.methods.withdraw(proof, ...args).send({ from: web3.eth.defaultAccount, gas: 1e6 })
|
||||
console.log(`https://kovan.etherscan.io/tx/${tx.transactionHash}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Tornado.cash note
|
||||
* @param noteString the note
|
||||
*/
|
||||
function parseNote(noteString) {
|
||||
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g
|
||||
const match = noteRegex.exec(noteString)
|
||||
|
||||
// we are ignoring `currency`, `amount`, and `netId` for this minimal example
|
||||
const buf = Buffer.from(match.groups.note, 'hex')
|
||||
const nullifier = bigInt.leBuff2int(buf.slice(0, 31))
|
||||
const secret = bigInt.leBuff2int(buf.slice(31, 62))
|
||||
return createDeposit(nullifier, secret)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate merkle tree for a deposit.
|
||||
* Download deposit events from the contract, reconstructs merkle tree, finds our deposit leaf
|
||||
* in it and generates merkle proof
|
||||
* @param deposit Deposit object
|
||||
*/
|
||||
async function generateMerkleProof(deposit) {
|
||||
console.log('Getting contract state...')
|
||||
const events = await contract.getPastEvents('Deposit', { fromBlock: 0, toBlock: 'latest' })
|
||||
const leaves = events
|
||||
.sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order
|
||||
.map((e) => e.returnValues.commitment)
|
||||
const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves)
|
||||
|
||||
// Find current commitment in the tree
|
||||
let depositEvent = events.find((e) => e.returnValues.commitment === toHex(deposit.commitment))
|
||||
let leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
|
||||
|
||||
// Validate that our data is correct (optional)
|
||||
const isValidRoot = await contract.methods.isKnownRoot(toHex(tree.root())).call()
|
||||
const isSpent = await contract.methods.isSpent(toHex(deposit.nullifierHash)).call()
|
||||
assert(isValidRoot === true, 'Merkle tree is corrupted')
|
||||
assert(isSpent === false, 'The note is already spent')
|
||||
assert(leafIndex >= 0, 'The deposit is not found in the tree')
|
||||
|
||||
// Compute merkle proof of our commitment
|
||||
const { pathElements, pathIndices } = tree.path(leafIndex)
|
||||
return { pathElements, pathIndices, root: tree.root() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SNARK proof for withdrawal
|
||||
* @param deposit Deposit object
|
||||
* @param recipient Funds recipient
|
||||
*/
|
||||
async function generateSnarkProof(deposit, recipient) {
|
||||
// Compute merkle proof of our commitment
|
||||
const { root, pathElements, pathIndices } = await generateMerkleProof(deposit)
|
||||
|
||||
// Prepare circuit input
|
||||
const input = {
|
||||
// Public snark inputs
|
||||
root: root,
|
||||
nullifierHash: deposit.nullifierHash,
|
||||
recipient: bigInt(recipient),
|
||||
relayer: 0,
|
||||
fee: 0,
|
||||
refund: 0,
|
||||
|
||||
// Private snark inputs
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
}
|
||||
|
||||
console.log('Generating SNARK proof...')
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const args = [
|
||||
toHex(input.root),
|
||||
toHex(input.nullifierHash),
|
||||
toHex(input.recipient, 20),
|
||||
toHex(input.relayer, 20),
|
||||
toHex(input.fee),
|
||||
toHex(input.refund),
|
||||
]
|
||||
|
||||
return { proof, args }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
web3 = new Web3(new Web3.providers.HttpProvider(RPC_URL, { timeout: 5 * 60 * 1000 }), null, {
|
||||
transactionConfirmationBlocks: 1,
|
||||
})
|
||||
circuit = require(__dirname + '/../build/circuits/withdraw.json')
|
||||
proving_key = fs.readFileSync(__dirname + '/../build/circuits/withdraw_proving_key.bin').buffer
|
||||
groth16 = await buildGroth16()
|
||||
netId = await web3.eth.net.getId()
|
||||
contract = new web3.eth.Contract(require('../build/contracts/ETHTornado.json').abi, CONTRACT_ADDRESS)
|
||||
const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY)
|
||||
web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
web3.eth.defaultAccount = account.address
|
||||
|
||||
const note = await deposit()
|
||||
console.log('Deposited note:', note)
|
||||
await withdraw(note, web3.eth.defaultAccount)
|
||||
console.log('Done')
|
||||
process.exit()
|
||||
}
|
||||
|
||||
main()
|
|
@ -0,0 +1,505 @@
|
|||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
const fs = require('fs')
|
||||
|
||||
const { toBN } = require('web3-utils')
|
||||
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
|
||||
|
||||
const Tornado = artifacts.require('./ERC20Tornado.sol')
|
||||
const BadRecipient = artifacts.require('./BadRecipient.sol')
|
||||
const Token = artifacts.require('./ERC20Mock.sol')
|
||||
const USDTToken = artifacts.require('./IUSDT.sol')
|
||||
const { ETH_AMOUNT, TOKEN_AMOUNT, MERKLE_TREE_HEIGHT, ERC20_TOKEN } = process.env
|
||||
|
||||
const websnarkUtils = require('websnark/src/utils')
|
||||
const buildGroth16 = require('websnark/src/groth16')
|
||||
const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts
|
||||
const snarkjs = require('snarkjs')
|
||||
const bigInt = snarkjs.bigInt
|
||||
const crypto = require('crypto')
|
||||
const circomlib = require('circomlib')
|
||||
const MerkleTree = require('fixed-merkle-tree')
|
||||
|
||||
const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
|
||||
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
|
||||
const toFixedHex = (number, length = 32) =>
|
||||
'0x' +
|
||||
bigInt(number)
|
||||
.toString(16)
|
||||
.padStart(length * 2, '0')
|
||||
const getRandomRecipient = () => rbigint(20)
|
||||
|
||||
function generateDeposit() {
|
||||
let deposit = {
|
||||
secret: rbigint(31),
|
||||
nullifier: rbigint(31),
|
||||
}
|
||||
const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
|
||||
deposit.commitment = pedersenHash(preimage)
|
||||
return deposit
|
||||
}
|
||||
|
||||
contract('ERC20Tornado', (accounts) => {
|
||||
let tornado
|
||||
let token
|
||||
let usdtToken
|
||||
let badRecipient
|
||||
const sender = accounts[0]
|
||||
const operator = accounts[0]
|
||||
const levels = MERKLE_TREE_HEIGHT || 16
|
||||
let tokenDenomination = TOKEN_AMOUNT || '1000000000000000000' // 1 ether
|
||||
let snapshotId
|
||||
let tree
|
||||
const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
|
||||
const refund = ETH_AMOUNT || '1000000000000000000' // 1 ether
|
||||
let recipient = getRandomRecipient()
|
||||
const relayer = accounts[1]
|
||||
let groth16
|
||||
let circuit
|
||||
let proving_key
|
||||
|
||||
before(async () => {
|
||||
tree = new MerkleTree(levels)
|
||||
tornado = await Tornado.deployed()
|
||||
if (ERC20_TOKEN) {
|
||||
token = await Token.at(ERC20_TOKEN)
|
||||
usdtToken = await USDTToken.at(ERC20_TOKEN)
|
||||
} else {
|
||||
token = await Token.deployed()
|
||||
await token.mint(sender, tokenDenomination)
|
||||
}
|
||||
badRecipient = await BadRecipient.new()
|
||||
snapshotId = await takeSnapshot()
|
||||
groth16 = await buildGroth16()
|
||||
circuit = require('../build/circuits/withdraw.json')
|
||||
proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
|
||||
})
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should initialize', async () => {
|
||||
const tokenFromContract = await tornado.token()
|
||||
tokenFromContract.should.be.equal(token.address)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#deposit', () => {
|
||||
it('should work', async () => {
|
||||
const commitment = toFixedHex(43)
|
||||
await token.approve(tornado.address, tokenDenomination)
|
||||
|
||||
let { logs } = await tornado.deposit(commitment, { from: sender })
|
||||
|
||||
logs[0].event.should.be.equal('Deposit')
|
||||
logs[0].args.commitment.should.be.equal(commitment)
|
||||
logs[0].args.leafIndex.should.be.eq.BN(0)
|
||||
})
|
||||
|
||||
it('should not allow to send ether on deposit', async () => {
|
||||
const commitment = toFixedHex(43)
|
||||
await token.approve(tornado.address, tokenDenomination)
|
||||
|
||||
let error = await tornado.deposit(commitment, { from: sender, value: 1e6 }).should.be.rejected
|
||||
error.reason.should.be.equal('ETH value is supposed to be 0 for ERC20 instance')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#withdraw', () => {
|
||||
it('should work', async () => {
|
||||
const deposit = generateDeposit()
|
||||
const user = accounts[4]
|
||||
tree.insert(deposit.commitment)
|
||||
await token.mint(user, tokenDenomination)
|
||||
|
||||
const balanceUserBefore = await token.balanceOf(user)
|
||||
await token.approve(tornado.address, tokenDenomination, { from: user })
|
||||
// Uncomment to measure gas usage
|
||||
// let gas = await tornado.deposit.estimateGas(toBN(deposit.commitment.toString()), { from: user, gasPrice: '0' })
|
||||
// console.log('deposit gas:', gas)
|
||||
await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
|
||||
|
||||
const balanceUserAfter = await token.balanceOf(user)
|
||||
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
|
||||
|
||||
const { pathElements, pathIndices } = tree.path(0)
|
||||
// Circuit input
|
||||
const input = stringifyBigInts({
|
||||
// public
|
||||
root: tree.root(),
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
relayer,
|
||||
recipient,
|
||||
fee,
|
||||
refund,
|
||||
|
||||
// private
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const balanceTornadoBefore = await token.balanceOf(tornado.address)
|
||||
const balanceRelayerBefore = await token.balanceOf(relayer)
|
||||
const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20))
|
||||
|
||||
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
|
||||
const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
|
||||
const ethBalanceRelayerBefore = await web3.eth.getBalance(relayer)
|
||||
let isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
|
||||
isSpent.should.be.equal(false)
|
||||
// Uncomment to measure gas usage
|
||||
// gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
|
||||
// console.log('withdraw gas:', gas)
|
||||
const args = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
|
||||
|
||||
const balanceTornadoAfter = await token.balanceOf(tornado.address)
|
||||
const balanceRelayerAfter = await token.balanceOf(relayer)
|
||||
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
|
||||
const balanceReceiverAfter = await token.balanceOf(toFixedHex(recipient, 20))
|
||||
const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
|
||||
const ethBalanceRelayerAfter = await web3.eth.getBalance(relayer)
|
||||
const feeBN = toBN(fee.toString())
|
||||
balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(tokenDenomination)))
|
||||
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore).add(feeBN))
|
||||
balanceReceiverAfter.should.be.eq.BN(
|
||||
toBN(balanceReceiverBefore).add(toBN(tokenDenomination).sub(feeBN)),
|
||||
)
|
||||
|
||||
ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore))
|
||||
ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore).add(toBN(refund)))
|
||||
ethBalanceRelayerAfter.should.be.eq.BN(toBN(ethBalanceRelayerBefore).sub(toBN(refund)))
|
||||
|
||||
logs[0].event.should.be.equal('Withdrawal')
|
||||
logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash))
|
||||
logs[0].args.relayer.should.be.eq.BN(relayer)
|
||||
logs[0].args.fee.should.be.eq.BN(feeBN)
|
||||
isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
|
||||
isSpent.should.be.equal(true)
|
||||
})
|
||||
|
||||
it('should return refund to the relayer is case of fail', async () => {
|
||||
const deposit = generateDeposit()
|
||||
const user = accounts[4]
|
||||
recipient = bigInt(badRecipient.address)
|
||||
tree.insert(deposit.commitment)
|
||||
await token.mint(user, tokenDenomination)
|
||||
|
||||
const balanceUserBefore = await token.balanceOf(user)
|
||||
await token.approve(tornado.address, tokenDenomination, { from: user })
|
||||
await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
|
||||
|
||||
const balanceUserAfter = await token.balanceOf(user)
|
||||
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
|
||||
|
||||
const { pathElements, pathIndices } = tree.path(0)
|
||||
// Circuit input
|
||||
const input = stringifyBigInts({
|
||||
// public
|
||||
root: tree.root(),
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
relayer,
|
||||
recipient,
|
||||
fee,
|
||||
refund,
|
||||
|
||||
// private
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const balanceTornadoBefore = await token.balanceOf(tornado.address)
|
||||
const balanceRelayerBefore = await token.balanceOf(relayer)
|
||||
const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20))
|
||||
|
||||
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
|
||||
const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
|
||||
const ethBalanceRelayerBefore = await web3.eth.getBalance(relayer)
|
||||
let isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
|
||||
isSpent.should.be.equal(false)
|
||||
|
||||
const args = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
|
||||
|
||||
const balanceTornadoAfter = await token.balanceOf(tornado.address)
|
||||
const balanceRelayerAfter = await token.balanceOf(relayer)
|
||||
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
|
||||
const balanceReceiverAfter = await token.balanceOf(toFixedHex(recipient, 20))
|
||||
const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
|
||||
const ethBalanceRelayerAfter = await web3.eth.getBalance(relayer)
|
||||
const feeBN = toBN(fee.toString())
|
||||
balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(tokenDenomination)))
|
||||
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore).add(feeBN))
|
||||
balanceReceiverAfter.should.be.eq.BN(
|
||||
toBN(balanceReceiverBefore).add(toBN(tokenDenomination).sub(feeBN)),
|
||||
)
|
||||
|
||||
ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore))
|
||||
ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore))
|
||||
ethBalanceRelayerAfter.should.be.eq.BN(toBN(ethBalanceRelayerBefore))
|
||||
|
||||
logs[0].event.should.be.equal('Withdrawal')
|
||||
logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash))
|
||||
logs[0].args.relayer.should.be.eq.BN(relayer)
|
||||
logs[0].args.fee.should.be.eq.BN(feeBN)
|
||||
isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
|
||||
isSpent.should.be.equal(true)
|
||||
})
|
||||
|
||||
it('should reject with wrong refund value', async () => {
|
||||
const deposit = generateDeposit()
|
||||
const user = accounts[4]
|
||||
tree.insert(deposit.commitment)
|
||||
await token.mint(user, tokenDenomination)
|
||||
await token.approve(tornado.address, tokenDenomination, { from: user })
|
||||
await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
|
||||
|
||||
const { pathElements, pathIndices } = tree.path(0)
|
||||
// Circuit input
|
||||
const input = stringifyBigInts({
|
||||
// public
|
||||
root: tree.root(),
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
relayer,
|
||||
recipient,
|
||||
fee,
|
||||
refund,
|
||||
|
||||
// private
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const args = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
let { reason } = await tornado.withdraw(proof, ...args, { value: 1, from: relayer, gasPrice: '0' })
|
||||
.should.be.rejected
|
||||
reason.should.be.equal('Incorrect refund amount received by the contract')
|
||||
;({ reason } = await tornado.withdraw(proof, ...args, {
|
||||
value: toBN(refund).mul(toBN(2)),
|
||||
from: relayer,
|
||||
gasPrice: '0',
|
||||
}).should.be.rejected)
|
||||
reason.should.be.equal('Incorrect refund amount received by the contract')
|
||||
})
|
||||
|
||||
it.skip('should work with REAL USDT', async () => {
|
||||
// dont forget to specify your token in .env
|
||||
// USDT decimals is 6, so TOKEN_AMOUNT=1000000
|
||||
// and sent `tokenDenomination` to accounts[0] (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1)
|
||||
// run ganache as
|
||||
// ganache-cli --fork https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448@13147586 -d --keepAliveTimeout 20
|
||||
const deposit = generateDeposit()
|
||||
const user = accounts[4]
|
||||
const userBal = await usdtToken.balanceOf(user)
|
||||
console.log('userBal', userBal.toString())
|
||||
const senderBal = await usdtToken.balanceOf(sender)
|
||||
console.log('senderBal', senderBal.toString())
|
||||
tree.insert(deposit.commitment)
|
||||
await usdtToken.transfer(user, tokenDenomination, { from: sender })
|
||||
console.log('transfer done')
|
||||
|
||||
const balanceUserBefore = await usdtToken.balanceOf(user)
|
||||
console.log('balanceUserBefore', balanceUserBefore.toString())
|
||||
await usdtToken.approve(tornado.address, tokenDenomination, { from: user })
|
||||
console.log('approve done')
|
||||
const allowanceUser = await usdtToken.allowance(user, tornado.address)
|
||||
console.log('allowanceUser', allowanceUser.toString())
|
||||
await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
|
||||
console.log('deposit done')
|
||||
|
||||
const balanceUserAfter = await usdtToken.balanceOf(user)
|
||||
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
|
||||
|
||||
const { pathElements, pathIndices } = tree.path(0)
|
||||
|
||||
// Circuit input
|
||||
const input = stringifyBigInts({
|
||||
// public
|
||||
root: tree.root(),
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
relayer: operator,
|
||||
recipient,
|
||||
fee,
|
||||
refund,
|
||||
|
||||
// private
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const balanceTornadoBefore = await usdtToken.balanceOf(tornado.address)
|
||||
const balanceRelayerBefore = await usdtToken.balanceOf(relayer)
|
||||
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
|
||||
const balanceReceiverBefore = await usdtToken.balanceOf(toFixedHex(recipient, 20))
|
||||
const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
|
||||
let isSpent = await tornado.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
|
||||
isSpent.should.be.equal(false)
|
||||
|
||||
// Uncomment to measure gas usage
|
||||
// gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
|
||||
// console.log('withdraw gas:', gas)
|
||||
const args = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
|
||||
|
||||
const balanceTornadoAfter = await usdtToken.balanceOf(tornado.address)
|
||||
const balanceRelayerAfter = await usdtToken.balanceOf(relayer)
|
||||
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
|
||||
const balanceReceiverAfter = await usdtToken.balanceOf(toFixedHex(recipient, 20))
|
||||
const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
|
||||
const feeBN = toBN(fee.toString())
|
||||
balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(tokenDenomination)))
|
||||
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
|
||||
ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore).add(feeBN))
|
||||
balanceReceiverAfter.should.be.eq.BN(toBN(balanceReceiverBefore).add(toBN(tokenDenomination)))
|
||||
ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore).add(toBN(refund)).sub(feeBN))
|
||||
|
||||
logs[0].event.should.be.equal('Withdrawal')
|
||||
logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString()))
|
||||
logs[0].args.relayer.should.be.eq.BN(operator)
|
||||
logs[0].args.fee.should.be.eq.BN(feeBN)
|
||||
isSpent = await tornado.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
|
||||
isSpent.should.be.equal(true)
|
||||
})
|
||||
it.skip('should work with REAL DAI', async () => {
|
||||
// dont forget to specify your token in .env
|
||||
// and send `tokenDenomination` to accounts[0] (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1)
|
||||
// run ganache as
|
||||
// npx ganache-cli --fork https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448@13146218 -d --keepAliveTimeout 20
|
||||
const deposit = generateDeposit()
|
||||
const user = accounts[4]
|
||||
const userBal = await token.balanceOf(user)
|
||||
console.log('userBal', userBal.toString())
|
||||
const senderBal = await token.balanceOf(sender)
|
||||
console.log('senderBal', senderBal.toString())
|
||||
tree.insert(deposit.commitment)
|
||||
await token.transfer(user, tokenDenomination, { from: sender })
|
||||
console.log('transfer done')
|
||||
|
||||
const balanceUserBefore = await token.balanceOf(user)
|
||||
console.log('balanceUserBefore', balanceUserBefore.toString())
|
||||
await token.approve(tornado.address, tokenDenomination, { from: user })
|
||||
console.log('approve done')
|
||||
await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
|
||||
console.log('deposit done')
|
||||
|
||||
const balanceUserAfter = await token.balanceOf(user)
|
||||
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
|
||||
|
||||
const { pathElements, pathIndices } = tree.path(0)
|
||||
|
||||
// Circuit input
|
||||
const input = stringifyBigInts({
|
||||
// public
|
||||
root: tree.root(),
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
relayer: operator,
|
||||
recipient,
|
||||
fee,
|
||||
refund,
|
||||
|
||||
// private
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const balanceTornadoBefore = await token.balanceOf(tornado.address)
|
||||
const balanceRelayerBefore = await token.balanceOf(relayer)
|
||||
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
|
||||
const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20))
|
||||
const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
|
||||
let isSpent = await tornado.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
|
||||
isSpent.should.be.equal(false)
|
||||
|
||||
// Uncomment to measure gas usage
|
||||
// gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
|
||||
// console.log('withdraw gas:', gas)
|
||||
const args = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
|
||||
console.log('withdraw done')
|
||||
|
||||
const balanceTornadoAfter = await token.balanceOf(tornado.address)
|
||||
const balanceRelayerAfter = await token.balanceOf(relayer)
|
||||
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
|
||||
const balanceReceiverAfter = await token.balanceOf(toFixedHex(recipient, 20))
|
||||
const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
|
||||
const feeBN = toBN(fee.toString())
|
||||
balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(tokenDenomination)))
|
||||
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
|
||||
ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore).add(feeBN))
|
||||
balanceReceiverAfter.should.be.eq.BN(toBN(balanceReceiverBefore).add(toBN(tokenDenomination)))
|
||||
ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore).add(toBN(refund)).sub(feeBN))
|
||||
|
||||
logs[0].event.should.be.equal('Withdrawal')
|
||||
logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString()))
|
||||
logs[0].args.relayer.should.be.eq.BN(operator)
|
||||
logs[0].args.fee.should.be.eq.BN(feeBN)
|
||||
isSpent = await tornado.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
|
||||
isSpent.should.be.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
tree = new MerkleTree(levels)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,540 @@
|
|||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
const fs = require('fs')
|
||||
|
||||
const { toBN, randomHex } = require('web3-utils')
|
||||
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
|
||||
|
||||
const Tornado = artifacts.require('./ETHTornado.sol')
|
||||
const { ETH_AMOUNT, MERKLE_TREE_HEIGHT } = process.env
|
||||
|
||||
const websnarkUtils = require('websnark/src/utils')
|
||||
const buildGroth16 = require('websnark/src/groth16')
|
||||
const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts
|
||||
const unstringifyBigInts2 = require('snarkjs/src/stringifybigint').unstringifyBigInts
|
||||
const snarkjs = require('snarkjs')
|
||||
const bigInt = snarkjs.bigInt
|
||||
const crypto = require('crypto')
|
||||
const circomlib = require('circomlib')
|
||||
const MerkleTree = require('fixed-merkle-tree')
|
||||
|
||||
const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
|
||||
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
|
||||
const toFixedHex = (number, length = 32) =>
|
||||
'0x' +
|
||||
bigInt(number)
|
||||
.toString(16)
|
||||
.padStart(length * 2, '0')
|
||||
const getRandomRecipient = () => rbigint(20)
|
||||
|
||||
function generateDeposit() {
|
||||
let deposit = {
|
||||
secret: rbigint(31),
|
||||
nullifier: rbigint(31),
|
||||
}
|
||||
const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
|
||||
deposit.commitment = pedersenHash(preimage)
|
||||
return deposit
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function BNArrayToStringArray(array) {
|
||||
const arrayToPrint = []
|
||||
array.forEach((item) => {
|
||||
arrayToPrint.push(item.toString())
|
||||
})
|
||||
return arrayToPrint
|
||||
}
|
||||
|
||||
function snarkVerify(proof) {
|
||||
proof = unstringifyBigInts2(proof)
|
||||
const verification_key = unstringifyBigInts2(require('../build/circuits/withdraw_verification_key.json'))
|
||||
return snarkjs['groth'].isValid(verification_key, proof, proof.publicSignals)
|
||||
}
|
||||
|
||||
contract('ETHTornado', (accounts) => {
|
||||
let tornado
|
||||
const sender = accounts[0]
|
||||
const operator = accounts[0]
|
||||
const levels = MERKLE_TREE_HEIGHT || 16
|
||||
const value = ETH_AMOUNT || '1000000000000000000' // 1 ether
|
||||
let snapshotId
|
||||
let tree
|
||||
const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
|
||||
const refund = bigInt(0)
|
||||
const recipient = getRandomRecipient()
|
||||
const relayer = accounts[1]
|
||||
let groth16
|
||||
let circuit
|
||||
let proving_key
|
||||
|
||||
before(async () => {
|
||||
tree = new MerkleTree(levels)
|
||||
tornado = await Tornado.deployed()
|
||||
snapshotId = await takeSnapshot()
|
||||
groth16 = await buildGroth16()
|
||||
circuit = require('../build/circuits/withdraw.json')
|
||||
proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
|
||||
})
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should initialize', async () => {
|
||||
const etherDenomination = await tornado.denomination()
|
||||
etherDenomination.should.be.eq.BN(toBN(value))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#deposit', () => {
|
||||
it('should emit event', async () => {
|
||||
let commitment = toFixedHex(42)
|
||||
let { logs } = await tornado.deposit(commitment, { value, from: sender })
|
||||
|
||||
logs[0].event.should.be.equal('Deposit')
|
||||
logs[0].args.commitment.should.be.equal(commitment)
|
||||
logs[0].args.leafIndex.should.be.eq.BN(0)
|
||||
|
||||
commitment = toFixedHex(12)
|
||||
;({ logs } = await tornado.deposit(commitment, { value, from: accounts[2] }))
|
||||
|
||||
logs[0].event.should.be.equal('Deposit')
|
||||
logs[0].args.commitment.should.be.equal(commitment)
|
||||
logs[0].args.leafIndex.should.be.eq.BN(1)
|
||||
})
|
||||
|
||||
it('should throw if there is a such commitment', async () => {
|
||||
const commitment = toFixedHex(42)
|
||||
await tornado.deposit(commitment, { value, from: sender }).should.be.fulfilled
|
||||
const error = await tornado.deposit(commitment, { value, from: sender }).should.be.rejected
|
||||
error.reason.should.be.equal('The commitment has been submitted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('snark proof verification on js side', () => {
|
||||
it('should detect tampering', async () => {
|
||||
const deposit = generateDeposit()
|
||||
tree.insert(deposit.commitment)
|
||||
const { pathElements, pathIndices } = tree.path(0)
|
||||
|
||||
const input = stringifyBigInts({
|
||||
root: tree.root(),
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
nullifier: deposit.nullifier,
|
||||
relayer: operator,
|
||||
recipient,
|
||||
fee,
|
||||
refund,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
|
||||
let proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const originalProof = JSON.parse(JSON.stringify(proofData))
|
||||
let result = snarkVerify(proofData)
|
||||
result.should.be.equal(true)
|
||||
|
||||
// nullifier
|
||||
proofData.publicSignals[1] =
|
||||
'133792158246920651341275668520530514036799294649489851421007411546007850802'
|
||||
result = snarkVerify(proofData)
|
||||
result.should.be.equal(false)
|
||||
proofData = originalProof
|
||||
|
||||
// try to cheat with recipient
|
||||
proofData.publicSignals[2] = '133738360804642228759657445999390850076318544422'
|
||||
result = snarkVerify(proofData)
|
||||
result.should.be.equal(false)
|
||||
proofData = originalProof
|
||||
|
||||
// fee
|
||||
proofData.publicSignals[3] = '1337100000000000000000'
|
||||
result = snarkVerify(proofData)
|
||||
result.should.be.equal(false)
|
||||
proofData = originalProof
|
||||
})
|
||||
})
|
||||
|
||||
describe('#withdraw', () => {
|
||||
it('should work', async () => {
|
||||
const deposit = generateDeposit()
|
||||
const user = accounts[4]
|
||||
tree.insert(deposit.commitment)
|
||||
|
||||
const balanceUserBefore = await web3.eth.getBalance(user)
|
||||
|
||||
// Uncomment to measure gas usage
|
||||
// let gas = await tornado.deposit.estimateGas(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
|
||||
// console.log('deposit gas:', gas)
|
||||
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: user, gasPrice: '0' })
|
||||
|
||||
const balanceUserAfter = await web3.eth.getBalance(user)
|
||||
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(value)))
|
||||
|
||||
const { pathElements, pathIndices } = tree.path(0)
|
||||
|
||||
// Circuit input
|
||||
const input = stringifyBigInts({
|
||||
// public
|
||||
root: tree.root(),
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
relayer: operator,
|
||||
recipient,
|
||||
fee,
|
||||
refund,
|
||||
|
||||
// private
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const balanceTornadoBefore = await web3.eth.getBalance(tornado.address)
|
||||
const balanceRelayerBefore = await web3.eth.getBalance(relayer)
|
||||
const balanceOperatorBefore = await web3.eth.getBalance(operator)
|
||||
const balanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
|
||||
let isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
|
||||
isSpent.should.be.equal(false)
|
||||
|
||||
// Uncomment to measure gas usage
|
||||
// gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
|
||||
// console.log('withdraw gas:', gas)
|
||||
const args = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
const { logs } = await tornado.withdraw(proof, ...args, { from: relayer, gasPrice: '0' })
|
||||
|
||||
const balanceTornadoAfter = await web3.eth.getBalance(tornado.address)
|
||||
const balanceRelayerAfter = await web3.eth.getBalance(relayer)
|
||||
const balanceOperatorAfter = await web3.eth.getBalance(operator)
|
||||
const balanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
|
||||
const feeBN = toBN(fee.toString())
|
||||
balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(value)))
|
||||
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
|
||||
balanceOperatorAfter.should.be.eq.BN(toBN(balanceOperatorBefore).add(feeBN))
|
||||
balanceReceiverAfter.should.be.eq.BN(toBN(balanceReceiverBefore).add(toBN(value)).sub(feeBN))
|
||||
|
||||
logs[0].event.should.be.equal('Withdrawal')
|
||||
logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash))
|
||||
logs[0].args.relayer.should.be.eq.BN(operator)
|
||||
logs[0].args.fee.should.be.eq.BN(feeBN)
|
||||
isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
|
||||
isSpent.should.be.equal(true)
|
||||
})
|
||||
|
||||
it('should prevent double spend', async () => {
|
||||
const deposit = generateDeposit()
|
||||
tree.insert(deposit.commitment)
|
||||
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
|
||||
|
||||
const { pathElements, pathIndices } = tree.path(0)
|
||||
|
||||
const input = stringifyBigInts({
|
||||
root: tree.root(),
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
nullifier: deposit.nullifier,
|
||||
relayer: operator,
|
||||
recipient,
|
||||
fee,
|
||||
refund,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
const args = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
await tornado.withdraw(proof, ...args, { from: relayer }).should.be.fulfilled
|
||||
const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('The note has been already spent')
|
||||
})
|
||||
|
||||
it('should prevent double spend with overflow', async () => {
|
||||
const deposit = generateDeposit()
|
||||
tree.insert(deposit.commitment)
|
||||
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
|
||||
|
||||
const { pathElements, pathIndices } = tree.path(0)
|
||||
|
||||
const input = stringifyBigInts({
|
||||
root: tree.root(),
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
nullifier: deposit.nullifier,
|
||||
relayer: operator,
|
||||
recipient,
|
||||
fee,
|
||||
refund,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
const args = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(
|
||||
toBN(input.nullifierHash).add(
|
||||
toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617'),
|
||||
),
|
||||
),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('verifier-gte-snark-scalar-field')
|
||||
})
|
||||
|
||||
it('fee should be less or equal transfer value', async () => {
|
||||
const deposit = generateDeposit()
|
||||
tree.insert(deposit.commitment)
|
||||
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
|
||||
|
||||
const { pathElements, pathIndices } = tree.path(0)
|
||||
const largeFee = bigInt(value).add(bigInt(1))
|
||||
const input = stringifyBigInts({
|
||||
root: tree.root(),
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
nullifier: deposit.nullifier,
|
||||
relayer: operator,
|
||||
recipient,
|
||||
fee: largeFee,
|
||||
refund,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
const args = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('Fee exceeds transfer value')
|
||||
})
|
||||
|
||||
it('should throw for corrupted merkle tree root', async () => {
|
||||
const deposit = generateDeposit()
|
||||
tree.insert(deposit.commitment)
|
||||
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
|
||||
|
||||
const { pathElements, pathIndices } = tree.path(0)
|
||||
|
||||
const input = stringifyBigInts({
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
root: tree.root(),
|
||||
nullifier: deposit.nullifier,
|
||||
relayer: operator,
|
||||
recipient,
|
||||
fee,
|
||||
refund,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const args = [
|
||||
toFixedHex(randomHex(32)),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('Cannot find your merkle root')
|
||||
})
|
||||
|
||||
it('should reject with tampered public inputs', async () => {
|
||||
const deposit = generateDeposit()
|
||||
tree.insert(deposit.commitment)
|
||||
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
|
||||
|
||||
let { pathElements, pathIndices } = tree.path(0)
|
||||
|
||||
const input = stringifyBigInts({
|
||||
root: tree.root(),
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
nullifier: deposit.nullifier,
|
||||
relayer: operator,
|
||||
recipient,
|
||||
fee,
|
||||
refund,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
let { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
const args = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
let incorrectArgs
|
||||
const originalProof = proof.slice()
|
||||
|
||||
// recipient
|
||||
incorrectArgs = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex('0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337', 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
let error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('Invalid withdraw proof')
|
||||
|
||||
// fee
|
||||
incorrectArgs = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex('0x000000000000000000000000000000000000000000000000015345785d8a0000'),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('Invalid withdraw proof')
|
||||
|
||||
// nullifier
|
||||
incorrectArgs = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex('0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad'),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('Invalid withdraw proof')
|
||||
|
||||
// proof itself
|
||||
proof = '0xbeef' + proof.substr(6)
|
||||
await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
|
||||
|
||||
// should work with original values
|
||||
await tornado.withdraw(originalProof, ...args, { from: relayer }).should.be.fulfilled
|
||||
})
|
||||
|
||||
it('should reject with non zero refund', async () => {
|
||||
const deposit = generateDeposit()
|
||||
tree.insert(deposit.commitment)
|
||||
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
|
||||
|
||||
const { pathElements, pathIndices } = tree.path(0)
|
||||
|
||||
const input = stringifyBigInts({
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
root: tree.root(),
|
||||
nullifier: deposit.nullifier,
|
||||
relayer: operator,
|
||||
recipient,
|
||||
fee,
|
||||
refund: bigInt(1),
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const args = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('Refund value is supposed to be zero for ETH instance')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#isSpent', () => {
|
||||
it('should work', async () => {
|
||||
const deposit1 = generateDeposit()
|
||||
const deposit2 = generateDeposit()
|
||||
tree.insert(deposit1.commitment)
|
||||
tree.insert(deposit2.commitment)
|
||||
await tornado.deposit(toFixedHex(deposit1.commitment), { value, gasPrice: '0' })
|
||||
await tornado.deposit(toFixedHex(deposit2.commitment), { value, gasPrice: '0' })
|
||||
|
||||
const { pathElements, pathIndices } = tree.path(1)
|
||||
|
||||
// Circuit input
|
||||
const input = stringifyBigInts({
|
||||
// public
|
||||
root: tree.root(),
|
||||
nullifierHash: pedersenHash(deposit2.nullifier.leInt2Buff(31)),
|
||||
relayer: operator,
|
||||
recipient,
|
||||
fee,
|
||||
refund,
|
||||
|
||||
// private
|
||||
nullifier: deposit2.nullifier,
|
||||
secret: deposit2.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
})
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const args = [
|
||||
toFixedHex(input.root),
|
||||
toFixedHex(input.nullifierHash),
|
||||
toFixedHex(input.recipient, 20),
|
||||
toFixedHex(input.relayer, 20),
|
||||
toFixedHex(input.fee),
|
||||
toFixedHex(input.refund),
|
||||
]
|
||||
|
||||
await tornado.withdraw(proof, ...args, { from: relayer, gasPrice: '0' })
|
||||
|
||||
const nullifierHash1 = toFixedHex(pedersenHash(deposit1.nullifier.leInt2Buff(31)))
|
||||
const nullifierHash2 = toFixedHex(pedersenHash(deposit2.nullifier.leInt2Buff(31)))
|
||||
const spentArray = await tornado.isSpentArray([nullifierHash1, nullifierHash2])
|
||||
spentArray.should.be.deep.equal([false, true])
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
tree = new MerkleTree(levels)
|
||||
})
|
||||
})
|
|
@ -1,164 +1,58 @@
|
|||
/* global artifacts, web3, contract, assert */
|
||||
require('chai')
|
||||
.use(require('bn-chai')(web3.utils.BN))
|
||||
.use(require('chai-as-promised'))
|
||||
.should()
|
||||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
|
||||
const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper')
|
||||
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
|
||||
|
||||
const MerkleTreeWithHistory = artifacts.require('./MerkleTreeWithHistoryMock.sol')
|
||||
const MiMC = artifacts.require('./MiMC.sol')
|
||||
const hasherContract = artifacts.require('./Hasher.sol')
|
||||
|
||||
const MerkleTree = require('../lib/MerkleTree')
|
||||
const MimcHasher = require('../lib/MiMC')
|
||||
const MerkleTree = require('fixed-merkle-tree')
|
||||
|
||||
const { AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env
|
||||
const snarkjs = require('snarkjs')
|
||||
const bigInt = snarkjs.bigInt
|
||||
|
||||
const { ETH_AMOUNT, MERKLE_TREE_HEIGHT } = process.env
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function BNArrayToStringArray(array) {
|
||||
const arrayToPrint = []
|
||||
array.forEach(item => {
|
||||
array.forEach((item) => {
|
||||
arrayToPrint.push(item.toString())
|
||||
})
|
||||
return arrayToPrint
|
||||
}
|
||||
|
||||
contract('MerkleTreeWithHistory', accounts => {
|
||||
function toFixedHex(number, length = 32) {
|
||||
let str = bigInt(number).toString(16)
|
||||
while (str.length < length * 2) str = '0' + str
|
||||
str = '0x' + str
|
||||
return str
|
||||
}
|
||||
|
||||
contract('MerkleTreeWithHistory', (accounts) => {
|
||||
let merkleTreeWithHistory
|
||||
let miMC
|
||||
let hasherInstance
|
||||
let levels = MERKLE_TREE_HEIGHT || 16
|
||||
let zeroValue = EMPTY_ELEMENT || 1337
|
||||
const sender = accounts[0]
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const value = AMOUNT || '1000000000000000000'
|
||||
const value = ETH_AMOUNT || '1000000000000000000'
|
||||
let snapshotId
|
||||
let prefix = 'test'
|
||||
let tree
|
||||
let hasher
|
||||
|
||||
before(async () => {
|
||||
tree = new MerkleTree(
|
||||
levels,
|
||||
zeroValue,
|
||||
null,
|
||||
prefix,
|
||||
)
|
||||
miMC = await MiMC.deployed()
|
||||
await MerkleTreeWithHistory.link(MiMC, miMC.address)
|
||||
merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, zeroValue)
|
||||
tree = new MerkleTree(levels)
|
||||
hasherInstance = await hasherContract.deployed()
|
||||
merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, hasherInstance.address)
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should initialize', async () => {
|
||||
const filled_subtrees = await merkleTreeWithHistory.filled_subtrees()
|
||||
filled_subtrees[0].should.be.eq.BN(zeroValue)
|
||||
const zeros = await merkleTreeWithHistory.zeros()
|
||||
zeros[0].should.be.eq.BN(zeroValue)
|
||||
})
|
||||
})
|
||||
|
||||
describe('merkleTreeLib', () => {
|
||||
it('index_to_key', () => {
|
||||
assert.equal(
|
||||
MerkleTree.index_to_key('test', 5, 20),
|
||||
'test_tree_5_20',
|
||||
)
|
||||
})
|
||||
|
||||
it('tests insert', async () => {
|
||||
hasher = new MimcHasher()
|
||||
tree = new MerkleTree(
|
||||
2,
|
||||
zeroValue,
|
||||
null,
|
||||
prefix,
|
||||
)
|
||||
await tree.insert('5')
|
||||
let { root, path_elements } = await tree.path(0)
|
||||
const calculated_root = hasher.hash(null,
|
||||
hasher.hash(null, '5', path_elements[0]),
|
||||
path_elements[1]
|
||||
)
|
||||
// console.log(root)
|
||||
assert.equal(root, calculated_root)
|
||||
})
|
||||
it('creation odd elements count', async () => {
|
||||
const elements = [12, 13, 14, 15, 16, 17, 18, 19, 20]
|
||||
for(const [, el] of Object.entries(elements)) {
|
||||
await tree.insert(el)
|
||||
}
|
||||
|
||||
const batchTree = new MerkleTree(
|
||||
levels,
|
||||
zeroValue,
|
||||
elements,
|
||||
prefix,
|
||||
)
|
||||
for(const [i] of Object.entries(elements)) {
|
||||
const pathViaConstructor = await batchTree.path(i)
|
||||
const pathViaUpdate = await tree.path(i)
|
||||
pathViaConstructor.should.be.deep.equal(pathViaUpdate)
|
||||
}
|
||||
})
|
||||
|
||||
it('should find an element', async () => {
|
||||
const elements = [12, 13, 14, 15, 16, 17, 18, 19, 20]
|
||||
for(const [, el] of Object.entries(elements)) {
|
||||
await tree.insert(el)
|
||||
}
|
||||
let index = tree.getIndexByElement(13)
|
||||
index.should.be.equal(1)
|
||||
|
||||
index = tree.getIndexByElement(19)
|
||||
index.should.be.equal(7)
|
||||
|
||||
index = tree.getIndexByElement(12)
|
||||
index.should.be.equal(0)
|
||||
|
||||
index = tree.getIndexByElement(20)
|
||||
index.should.be.equal(8)
|
||||
|
||||
index = tree.getIndexByElement(42)
|
||||
index.should.be.equal(false)
|
||||
})
|
||||
|
||||
it('creation even elements count', async () => {
|
||||
const elements = [12, 13, 14, 15, 16, 17]
|
||||
for(const [, el] of Object.entries(elements)) {
|
||||
await tree.insert(el)
|
||||
}
|
||||
|
||||
const batchTree = new MerkleTree(
|
||||
levels,
|
||||
zeroValue,
|
||||
elements,
|
||||
prefix,
|
||||
)
|
||||
for(const [i] of Object.entries(elements)) {
|
||||
const pathViaConstructor = await batchTree.path(i)
|
||||
const pathViaUpdate = await tree.path(i)
|
||||
pathViaConstructor.should.be.deep.equal(pathViaUpdate)
|
||||
}
|
||||
})
|
||||
|
||||
it.skip('creation using 30000 elements', () => {
|
||||
const elements = []
|
||||
for(let i = 1000; i < 31001; i++) {
|
||||
elements.push(i)
|
||||
}
|
||||
console.time('MerkleTree')
|
||||
tree = new MerkleTree(
|
||||
levels,
|
||||
zeroValue,
|
||||
elements,
|
||||
prefix,
|
||||
)
|
||||
console.timeEnd('MerkleTree')
|
||||
// 2,7 GHz Intel Core i7
|
||||
// 1000 : 1949.084ms
|
||||
// 10000: 19456.220ms
|
||||
// 30000: 63406.679ms
|
||||
const zeroValue = await merkleTreeWithHistory.ZERO_VALUE()
|
||||
const firstSubtree = await merkleTreeWithHistory.filledSubtrees(0)
|
||||
firstSubtree.should.be.equal(toFixedHex(zeroValue))
|
||||
const firstZero = await merkleTreeWithHistory.zeros(0)
|
||||
firstZero.should.be.equal(toFixedHex(zeroValue))
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -167,28 +61,57 @@ contract('MerkleTreeWithHistory', accounts => {
|
|||
let rootFromContract
|
||||
|
||||
for (let i = 1; i < 11; i++) {
|
||||
await merkleTreeWithHistory.insert(i, { from: sender })
|
||||
await tree.insert(i)
|
||||
let { root } = await tree.path(i - 1)
|
||||
await merkleTreeWithHistory.insert(toFixedHex(i), { from: sender })
|
||||
tree.insert(i)
|
||||
rootFromContract = await merkleTreeWithHistory.getLastRoot()
|
||||
root.should.be.equal(rootFromContract.toString())
|
||||
toFixedHex(tree.root()).should.be.equal(rootFromContract.toString())
|
||||
}
|
||||
})
|
||||
|
||||
it('should reject if tree is full', async () => {
|
||||
levels = 6
|
||||
zeroValue = 1337
|
||||
merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, zeroValue)
|
||||
const levels = 6
|
||||
const merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, hasherInstance.address)
|
||||
|
||||
for (let i = 0; i < 2**(levels - 1); i++) {
|
||||
await merkleTreeWithHistory.insert(i+42).should.be.fulfilled
|
||||
for (let i = 0; i < 2 ** levels; i++) {
|
||||
await merkleTreeWithHistory.insert(toFixedHex(i + 42)).should.be.fulfilled
|
||||
}
|
||||
|
||||
let error = await merkleTreeWithHistory.insert(1337).should.be.rejected
|
||||
error.reason.should.be.equal('Merkle tree is full')
|
||||
let error = await merkleTreeWithHistory.insert(toFixedHex(1337)).should.be.rejected
|
||||
error.reason.should.be.equal('Merkle tree is full. No more leaves can be added')
|
||||
|
||||
error = await merkleTreeWithHistory.insert(1).should.be.rejected
|
||||
error.reason.should.be.equal('Merkle tree is full')
|
||||
error = await merkleTreeWithHistory.insert(toFixedHex(1)).should.be.rejected
|
||||
error.reason.should.be.equal('Merkle tree is full. No more leaves can be added')
|
||||
})
|
||||
|
||||
it.skip('hasher gas', async () => {
|
||||
const levels = 6
|
||||
const merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels)
|
||||
const zeroValue = await merkleTreeWithHistory.zeroValue()
|
||||
|
||||
const gas = await merkleTreeWithHistory.hashLeftRight.estimateGas(zeroValue, zeroValue)
|
||||
console.log('gas', gas - 21000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#isKnownRoot', () => {
|
||||
it('should work', async () => {
|
||||
for (let i = 1; i < 5; i++) {
|
||||
await merkleTreeWithHistory.insert(toFixedHex(i), { from: sender }).should.be.fulfilled
|
||||
await tree.insert(i)
|
||||
let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(tree.root()))
|
||||
isKnown.should.be.equal(true)
|
||||
}
|
||||
|
||||
await merkleTreeWithHistory.insert(toFixedHex(42), { from: sender }).should.be.fulfilled
|
||||
// check outdated root
|
||||
let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(tree.root()))
|
||||
isKnown.should.be.equal(true)
|
||||
})
|
||||
|
||||
it('should not return uninitialized roots', async () => {
|
||||
await merkleTreeWithHistory.insert(toFixedHex(42), { from: sender }).should.be.fulfilled
|
||||
let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(0))
|
||||
isKnown.should.be.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -196,14 +119,6 @@ contract('MerkleTreeWithHistory', accounts => {
|
|||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
hasher = new MimcHasher()
|
||||
tree = new MerkleTree(
|
||||
levels,
|
||||
zeroValue,
|
||||
null,
|
||||
prefix,
|
||||
null,
|
||||
hasher,
|
||||
)
|
||||
tree = new MerkleTree(levels)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,427 +0,0 @@
|
|||
/* global artifacts, web3, contract */
|
||||
require('chai')
|
||||
.use(require('bn-chai')(web3.utils.BN))
|
||||
.use(require('chai-as-promised'))
|
||||
.should()
|
||||
const fs = require('fs')
|
||||
|
||||
const { toBN, toHex, randomHex } = require('web3-utils')
|
||||
const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper')
|
||||
|
||||
const Mixer = artifacts.require('./Mixer.sol')
|
||||
const { AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env
|
||||
|
||||
const websnarkUtils = require('websnark/src/utils')
|
||||
const buildGroth16 = require('websnark/src/groth16')
|
||||
const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts
|
||||
const unstringifyBigInts2 = require('snarkjs/src/stringifybigint').unstringifyBigInts
|
||||
const snarkjs = require('snarkjs')
|
||||
const bigInt = snarkjs.bigInt
|
||||
const crypto = require('crypto')
|
||||
const circomlib = require('circomlib')
|
||||
const MerkleTree = require('../lib/MerkleTree')
|
||||
|
||||
const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
|
||||
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
|
||||
|
||||
function generateDeposit() {
|
||||
let deposit = {
|
||||
secret: rbigint(31),
|
||||
nullifier: rbigint(31),
|
||||
}
|
||||
const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
|
||||
deposit.commitment = pedersenHash(preimage)
|
||||
return deposit
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function BNArrayToStringArray(array) {
|
||||
const arrayToPrint = []
|
||||
array.forEach(item => {
|
||||
arrayToPrint.push(item.toString())
|
||||
})
|
||||
return arrayToPrint
|
||||
}
|
||||
|
||||
function getRandomReceiver() {
|
||||
let receiver = rbigint(20)
|
||||
while (toHex(receiver.toString()).length !== 42) {
|
||||
receiver = rbigint(20)
|
||||
}
|
||||
return receiver
|
||||
}
|
||||
|
||||
function snarkVerify(proof) {
|
||||
proof = unstringifyBigInts2(websnarkUtils.fromSolidityInput(proof))
|
||||
const verification_key = unstringifyBigInts2(require('../build/circuits/withdraw_verification_key.json'))
|
||||
return snarkjs['groth'].isValid(verification_key, proof, proof.publicSignals)
|
||||
}
|
||||
|
||||
contract('Mixer', accounts => {
|
||||
let mixer
|
||||
const sender = accounts[0]
|
||||
const operator = accounts[0]
|
||||
const levels = MERKLE_TREE_HEIGHT || 16
|
||||
const zeroValue = EMPTY_ELEMENT || 1337
|
||||
const value = AMOUNT || '1000000000000000000' // 1 ether
|
||||
let snapshotId
|
||||
let prefix = 'test'
|
||||
let tree
|
||||
const fee = bigInt(AMOUNT).shr(1) || bigInt(1e17)
|
||||
const receiver = getRandomReceiver()
|
||||
const relayer = accounts[1]
|
||||
let groth16
|
||||
let circuit
|
||||
let proving_key
|
||||
|
||||
before(async () => {
|
||||
tree = new MerkleTree(
|
||||
levels,
|
||||
zeroValue,
|
||||
null,
|
||||
prefix,
|
||||
)
|
||||
mixer = await Mixer.deployed()
|
||||
snapshotId = await takeSnapshot()
|
||||
groth16 = await buildGroth16()
|
||||
circuit = require('../build/circuits/withdraw.json')
|
||||
proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
|
||||
})
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should initialize', async () => {
|
||||
const transferValue = await mixer.transferValue()
|
||||
transferValue.should.be.eq.BN(toBN(value))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#deposit', () => {
|
||||
it('should emit event', async () => {
|
||||
let commitment = 42
|
||||
let { logs } = await mixer.deposit(commitment, { value, from: sender })
|
||||
|
||||
logs[0].event.should.be.equal('Deposit')
|
||||
logs[0].args.commitment.should.be.eq.BN(toBN(commitment))
|
||||
logs[0].args.leafIndex.should.be.eq.BN(toBN(0))
|
||||
|
||||
commitment = 12;
|
||||
({ logs } = await mixer.deposit(commitment, { value, from: accounts[2] }))
|
||||
|
||||
logs[0].event.should.be.equal('Deposit')
|
||||
logs[0].args.commitment.should.be.eq.BN(toBN(commitment))
|
||||
logs[0].args.leafIndex.should.be.eq.BN(toBN(1))
|
||||
})
|
||||
|
||||
it('should not deposit if disabled', async () => {
|
||||
let commitment = 42;
|
||||
(await mixer.isDepositsEnabled()).should.be.equal(true)
|
||||
const err = await mixer.toggleDeposits({ from: accounts[1] }).should.be.rejected
|
||||
err.reason.should.be.equal('unauthorized')
|
||||
await mixer.toggleDeposits({ from: sender });
|
||||
(await mixer.isDepositsEnabled()).should.be.equal(false)
|
||||
let error = await mixer.deposit(commitment, { value, from: sender }).should.be.rejected
|
||||
error.reason.should.be.equal('deposits disabled')
|
||||
})
|
||||
|
||||
it('should throw if there is a such commitment', async () => {
|
||||
const commitment = 42
|
||||
await mixer.deposit(commitment, { value, from: sender }).should.be.fulfilled
|
||||
const error = await mixer.deposit(commitment, { value, from: sender }).should.be.rejected
|
||||
error.reason.should.be.equal('The commitment has been submitted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('snark proof verification on js side', () => {
|
||||
it('should detect tampering', async () => {
|
||||
const deposit = generateDeposit()
|
||||
await tree.insert(deposit.commitment)
|
||||
const { root, path_elements, path_index } = await tree.path(0)
|
||||
|
||||
const input = stringifyBigInts({
|
||||
root,
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
nullifier: deposit.nullifier,
|
||||
receiver,
|
||||
fee,
|
||||
secret: deposit.secret,
|
||||
pathElements: path_elements,
|
||||
pathIndex: path_index,
|
||||
})
|
||||
|
||||
let proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const originalProof = JSON.parse(JSON.stringify(proof))
|
||||
let result = snarkVerify(proof)
|
||||
result.should.be.equal(true)
|
||||
|
||||
// nullifier
|
||||
proof.publicSignals[1] = '133792158246920651341275668520530514036799294649489851421007411546007850802'
|
||||
result = snarkVerify(proof)
|
||||
result.should.be.equal(false)
|
||||
proof = originalProof
|
||||
|
||||
// try to cheat with recipient
|
||||
proof.publicSignals[2] = '133738360804642228759657445999390850076318544422'
|
||||
result = snarkVerify(proof)
|
||||
result.should.be.equal(false)
|
||||
proof = originalProof
|
||||
|
||||
// fee
|
||||
proof.publicSignals[3] = '1337100000000000000000'
|
||||
result = snarkVerify(proof)
|
||||
result.should.be.equal(false)
|
||||
proof = originalProof
|
||||
})
|
||||
})
|
||||
|
||||
describe('#withdraw', () => {
|
||||
it('should work', async () => {
|
||||
const deposit = generateDeposit()
|
||||
const user = accounts[4]
|
||||
await tree.insert(deposit.commitment)
|
||||
|
||||
const balanceUserBefore = await web3.eth.getBalance(user)
|
||||
|
||||
// Uncomment to measure gas usage
|
||||
// let gas = await mixer.deposit.estimateGas(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
|
||||
// console.log('deposit gas:', gas)
|
||||
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
|
||||
|
||||
const balanceUserAfter = await web3.eth.getBalance(user)
|
||||
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(value)))
|
||||
|
||||
const { root, path_elements, path_index } = await tree.path(0)
|
||||
|
||||
// Circuit input
|
||||
const input = stringifyBigInts({
|
||||
// public
|
||||
root,
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
receiver,
|
||||
fee,
|
||||
|
||||
// private
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: path_elements,
|
||||
pathIndex: path_index,
|
||||
})
|
||||
|
||||
|
||||
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
|
||||
|
||||
const balanceMixerBefore = await web3.eth.getBalance(mixer.address)
|
||||
const balanceRelayerBefore = await web3.eth.getBalance(relayer)
|
||||
const balanceOperatorBefore = await web3.eth.getBalance(operator)
|
||||
const balanceRecieverBefore = await web3.eth.getBalance(toHex(receiver.toString()))
|
||||
let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
|
||||
isSpent.should.be.equal(false)
|
||||
|
||||
// Uncomment to measure gas usage
|
||||
// gas = await mixer.withdraw.estimateGas(pi_a, pi_b, pi_c, publicSignals, { from: relayer, gasPrice: '0' })
|
||||
// console.log('withdraw gas:', gas)
|
||||
const { logs } = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer, gasPrice: '0' })
|
||||
|
||||
const balanceMixerAfter = await web3.eth.getBalance(mixer.address)
|
||||
const balanceRelayerAfter = await web3.eth.getBalance(relayer)
|
||||
const balanceOperatorAfter = await web3.eth.getBalance(operator)
|
||||
const balanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString()))
|
||||
const feeBN = toBN(fee.toString())
|
||||
balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(value)))
|
||||
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
|
||||
balanceOperatorAfter.should.be.eq.BN(toBN(balanceOperatorBefore).add(feeBN))
|
||||
balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(value)).sub(feeBN))
|
||||
|
||||
|
||||
logs[0].event.should.be.equal('Withdraw')
|
||||
logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString()))
|
||||
logs[0].args.fee.should.be.eq.BN(feeBN)
|
||||
isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
|
||||
isSpent.should.be.equal(true)
|
||||
})
|
||||
|
||||
it('should prevent double spend', async () => {
|
||||
const deposit = generateDeposit()
|
||||
await tree.insert(deposit.commitment)
|
||||
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
|
||||
|
||||
const { root, path_elements, path_index } = await tree.path(0)
|
||||
|
||||
const input = stringifyBigInts({
|
||||
root,
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
nullifier: deposit.nullifier,
|
||||
receiver,
|
||||
fee,
|
||||
secret: deposit.secret,
|
||||
pathElements: path_elements,
|
||||
pathIndex: path_index,
|
||||
})
|
||||
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
|
||||
await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.fulfilled
|
||||
const error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('The note has been already spent')
|
||||
})
|
||||
|
||||
it('should prevent double spend with overflow', async () => {
|
||||
const deposit = generateDeposit()
|
||||
await tree.insert(deposit.commitment)
|
||||
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
|
||||
|
||||
const { root, path_elements, path_index } = await tree.path(0)
|
||||
|
||||
const input = stringifyBigInts({
|
||||
root,
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
nullifier: deposit.nullifier,
|
||||
receiver,
|
||||
fee,
|
||||
secret: deposit.secret,
|
||||
pathElements: path_elements,
|
||||
pathIndex: path_index,
|
||||
})
|
||||
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
|
||||
publicSignals[1] ='0x' + toBN(publicSignals[1]).add(toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617')).toString('hex')
|
||||
const error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('verifier-gte-snark-scalar-field')
|
||||
})
|
||||
|
||||
it('fee should be less or equal transfer value', async () => {
|
||||
const deposit = generateDeposit()
|
||||
await tree.insert(deposit.commitment)
|
||||
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
|
||||
|
||||
const { root, path_elements, path_index } = await tree.path(0)
|
||||
const oneEtherFee = bigInt(1e18) // 1 ether
|
||||
const input = stringifyBigInts({
|
||||
root,
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
nullifier: deposit.nullifier,
|
||||
receiver,
|
||||
fee: oneEtherFee,
|
||||
secret: deposit.secret,
|
||||
pathElements: path_elements,
|
||||
pathIndex: path_index,
|
||||
})
|
||||
|
||||
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
|
||||
const error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('Fee exceeds transfer value')
|
||||
})
|
||||
|
||||
it('should throw for corrupted merkle tree root', async () => {
|
||||
const deposit = generateDeposit()
|
||||
await tree.insert(deposit.commitment)
|
||||
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
|
||||
|
||||
const { root, path_elements, path_index } = await tree.path(0)
|
||||
|
||||
const input = stringifyBigInts({
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
root,
|
||||
nullifier: deposit.nullifier,
|
||||
receiver,
|
||||
fee,
|
||||
secret: deposit.secret,
|
||||
pathElements: path_elements,
|
||||
pathIndex: path_index,
|
||||
})
|
||||
|
||||
const dummyRoot = randomHex(32)
|
||||
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
|
||||
publicSignals[0] = dummyRoot
|
||||
|
||||
const error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('Cannot find your merkle root')
|
||||
})
|
||||
|
||||
it('should reject with tampered public inputs', async () => {
|
||||
const deposit = generateDeposit()
|
||||
await tree.insert(deposit.commitment)
|
||||
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
|
||||
|
||||
let { root, path_elements, path_index } = await tree.path(0)
|
||||
|
||||
const input = stringifyBigInts({
|
||||
root,
|
||||
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
|
||||
nullifier: deposit.nullifier,
|
||||
receiver,
|
||||
fee,
|
||||
secret: deposit.secret,
|
||||
pathElements: path_elements,
|
||||
pathIndex: path_index,
|
||||
})
|
||||
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
let { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
|
||||
const originalPublicSignals = publicSignals.slice()
|
||||
const originalPi_a = pi_a.slice()
|
||||
|
||||
// receiver
|
||||
publicSignals[2] = '0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337'
|
||||
|
||||
let error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('Invalid withdraw proof')
|
||||
|
||||
// fee
|
||||
publicSignals = originalPublicSignals.slice()
|
||||
publicSignals[3] = '0x000000000000000000000000000000000000000000000000015345785d8a0000'
|
||||
|
||||
error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('Invalid withdraw proof')
|
||||
|
||||
// nullifier
|
||||
publicSignals = originalPublicSignals.slice()
|
||||
publicSignals[1] = '0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad'
|
||||
|
||||
error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
|
||||
error.reason.should.be.equal('Invalid withdraw proof')
|
||||
|
||||
// proof itself
|
||||
pi_a[0] = '0x261d81d8203437f29b38a88c4263476d858e6d9645cf21740461684412b31337'
|
||||
await mixer.withdraw(pi_a, pi_b, pi_c, originalPublicSignals, { from: relayer }).should.be.rejected
|
||||
|
||||
// should work with original values
|
||||
await mixer.withdraw(originalPi_a, pi_b, pi_c, originalPublicSignals, { from: relayer }).should.be.fulfilled
|
||||
})
|
||||
})
|
||||
|
||||
describe('#changeOperator', () => {
|
||||
it('should work', async () => {
|
||||
let operator = await mixer.operator()
|
||||
operator.should.be.equal(sender)
|
||||
|
||||
const newOperator = accounts[7]
|
||||
await mixer.changeOperator(newOperator).should.be.fulfilled
|
||||
|
||||
operator = await mixer.operator()
|
||||
operator.should.be.equal(newOperator)
|
||||
})
|
||||
|
||||
it('cannot change from different address', async () => {
|
||||
let operator = await mixer.operator()
|
||||
operator.should.be.equal(sender)
|
||||
|
||||
const newOperator = accounts[7]
|
||||
const error = await mixer.changeOperator(newOperator, { from: accounts[7] }).should.be.rejected
|
||||
error.reason.should.be.equal('unauthorized')
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
tree = new MerkleTree(
|
||||
levels,
|
||||
zeroValue,
|
||||
null,
|
||||
prefix,
|
||||
)
|
||||
})
|
||||
})
|
|
@ -1,10 +1,6 @@
|
|||
require('dotenv').config()
|
||||
const HDWalletProvider = require('truffle-hdwallet-provider')
|
||||
const HDWalletProvider = require('@truffle/hdwallet-provider')
|
||||
const utils = require('web3-utils')
|
||||
// const infuraKey = "fj4jll3k.....";
|
||||
//
|
||||
// const fs = require('fs');
|
||||
// const mnemonic = fs.readFileSync(".secret").toString().trim();
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
|
@ -18,38 +14,49 @@ module.exports = {
|
|||
*/
|
||||
|
||||
networks: {
|
||||
// Useful for testing. The `development` name is special - truffle uses it by default
|
||||
// if it's defined here and no other network is specified at the command line.
|
||||
// You should run a client (like ganache-cli, geth or parity) in a separate terminal
|
||||
// tab if you use this network and you must also set the `host`, `port` and `network_id`
|
||||
// options below to some value.
|
||||
|
||||
development: {
|
||||
host: '127.0.0.1', // Localhost (default: none)
|
||||
port: 8545, // Standard Ethereum port (default: none)
|
||||
network_id: '*', // Any network (default: none)
|
||||
host: '127.0.0.1', // Localhost (default: none)
|
||||
port: 8545, // Standard Ethereum port (default: none)
|
||||
network_id: '*', // Any network (default: none)
|
||||
},
|
||||
|
||||
// Another network with more advanced options...
|
||||
// advanced: {
|
||||
// port: 8777, // Custom port
|
||||
// network_id: 1342, // Custom network
|
||||
// gas: 8500000, // Gas sent with each transaction (default: ~6700000)
|
||||
// gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei)
|
||||
// from: <address>, // Account to send txs from (default: accounts[0])
|
||||
// websockets: true // Enable EventEmitter interface for web3 (default: false)
|
||||
// },
|
||||
|
||||
// Useful for deploying to a public network.
|
||||
// NB: It's important to wrap the provider as a function.
|
||||
kovan: {
|
||||
provider: () => new HDWalletProvider(process.env.PRIVATE_KEY, 'https://kovan.infura.io/v3/c7463beadf2144e68646ff049917b716'),
|
||||
provider: () =>
|
||||
new HDWalletProvider(
|
||||
process.env.PRIVATE_KEY,
|
||||
'https://kovan.infura.io/v3/97c8bf358b9942a9853fab1ba93dc5b3',
|
||||
),
|
||||
network_id: 42,
|
||||
gas: 7000000,
|
||||
gas: 6000000,
|
||||
gasPrice: utils.toWei('1', 'gwei'),
|
||||
// confirmations: 0,
|
||||
// timeoutBlocks: 200,
|
||||
skipDryRun: true
|
||||
skipDryRun: true,
|
||||
},
|
||||
goerli: {
|
||||
provider: () =>
|
||||
new HDWalletProvider(
|
||||
process.env.PRIVATE_KEY,
|
||||
'https://goerli.infura.io/v3/d34c08f2cb7c4111b645d06ac7e35ba8',
|
||||
),
|
||||
network_id: 5,
|
||||
gas: 6000000,
|
||||
gasPrice: utils.toWei('1', 'gwei'),
|
||||
// confirmations: 0,
|
||||
// timeoutBlocks: 200,
|
||||
skipDryRun: true,
|
||||
},
|
||||
rinkeby: {
|
||||
provider: () =>
|
||||
new HDWalletProvider(
|
||||
process.env.PRIVATE_KEY,
|
||||
'https://rinkeby.infura.io/v3/97c8bf358b9942a9853fab1ba93dc5b3',
|
||||
),
|
||||
network_id: 4,
|
||||
gas: 6000000,
|
||||
gasPrice: utils.toWei('1', 'gwei'),
|
||||
// confirmations: 0,
|
||||
// timeoutBlocks: 200,
|
||||
skipDryRun: true,
|
||||
},
|
||||
mainnet: {
|
||||
provider: () => new HDWalletProvider(process.env.PRIVATE_KEY, 'http://ethereum-rpc.trustwalletapp.com'),
|
||||
|
@ -58,18 +65,10 @@ module.exports = {
|
|||
gasPrice: utils.toWei('2', 'gwei'),
|
||||
// confirmations: 0,
|
||||
// timeoutBlocks: 200,
|
||||
skipDryRun: true
|
||||
skipDryRun: true,
|
||||
},
|
||||
|
||||
// Useful for private networks
|
||||
// private: {
|
||||
// provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
|
||||
// network_id: 2111, // This network is yours, in the cloud.
|
||||
// production: true // Treats this network as if it was a public net. (default: false)
|
||||
// }
|
||||
},
|
||||
|
||||
// Set default mocha options here, use special reporters etc.
|
||||
mocha: {
|
||||
// timeout: 100000
|
||||
},
|
||||
|
@ -77,15 +76,23 @@ module.exports = {
|
|||
// Configure your compilers
|
||||
compilers: {
|
||||
solc: {
|
||||
version: '0.5.10', // Fetch exact version from solc-bin (default: truffle's version)
|
||||
// docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
|
||||
settings: { // See the solidity docs for advice about optimization and evmVersion
|
||||
version: '0.7.6',
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
runs: 200
|
||||
runs: 200,
|
||||
},
|
||||
// evmVersion: "byzantium"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
external: {
|
||||
command: 'node ./scripts/compileHasher.js',
|
||||
targets: [
|
||||
{
|
||||
path: './build/Hasher.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
plugins: ['solidity-coverage'],
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue