Compare commits

...

50 Commits
v2.1 ... master

Author SHA1 Message Date
Alexey Pertsev 1ef6a263ac
Merge pull request #97 from tornadocash/sol-coverage
add sol-coverage
2022-03-24 20:28:35 +01:00
Drygin f9f19b70e4 add coverage to CI 2022-03-24 21:48:38 +03:00
Drygin 10aeb05417 add sol-coverage 2022-03-05 20:33:06 +03:00
Roman Semenov 896fc224ff
Merge pull request #93 from HowJMay/typo
fix typos
2021-10-31 15:57:19 +00:00
HowJMay 0b8bbf6317 fix typos 2021-10-31 22:07:39 +08:00
poma 7d924e2447
fix lint 2021-10-29 19:22:40 +01:00
poma 2ec693d0de
fix cli 2021-10-29 18:58:55 +01:00
poma 0cf811b854
fix cli 2021-10-29 18:49:12 +01:00
poma f468de9a0a
fix test and readme 2021-10-29 18:37:09 +01:00
Roman Semenov 94dfad9cd2
Merge pull request #92 from mirru2532/instances
Allow mutability for state variables
2021-10-29 18:35:42 +01:00
poma 23543683f3
change dir structure 2021-10-29 18:23:56 +01:00
poma 0c6e638852
use merkle tree from npm 2021-10-29 18:20:26 +01:00
poma 801f29a4b7
update build badge 2021-10-29 18:09:46 +01:00
mirru2532 5cb9d60178 mycomm 2021-10-28 13:45:16 +02:00
Alexey 3603b1c9e1 update zeros func 2021-03-12 15:53:13 +04:00
Alexey c12643e2c2 make zeros immutable 2021-03-12 15:15:52 +04:00
poma 54a7bdcb04
update solidity to 0.7.6 2021-03-11 23:05:59 +03:00
poma f189a657c9
rename cPool 2021-03-11 22:56:02 +03:00
poma 3ad634594e
optimize sloads 2021-03-11 22:51:09 +03:00
Alexey 127a61e21f fix lint 2021-03-10 22:33:30 +04:00
Alexey c559a79396 update truffle; fix tests 2021-03-10 20:37:17 +04:00
Alexey 78bd4175fa update cPool 2021-03-10 20:28:44 +04:00
Alexey 4069b61421 cPool init 2021-03-10 13:27:15 +04:00
poma f5d8f6d971
revert verifier code to deployed one 2021-02-14 09:32:05 +03:00
poma 8580c5e427
deploy script 2021-02-11 10:29:50 +03:00
poma a359e86f85
fix tests, ci 2021-02-11 10:00:53 +03:00
poma 346ffcee3c
lint 2021-02-11 09:23:18 +03:00
poma c6b442713a
contracts fixed 2021-02-11 09:03:43 +03:00
Roman Storm 3c4def1e64
wip 2021-02-10 21:37:18 -08:00
Roman Semenov 77af0c5bdd
Merge pull request #68 from Lucienest/patch-1
Update README.md
2020-12-31 13:12:46 +03:00
Lucien Nocelli 4f0a23426f
Update README.md
Fixed few syntactical errors.
2020-12-31 06:47:45 +00:00
poma b438f8db7b
update audit links 2020-12-15 14:15:40 +03:00
Alexey Pertsev d02ff4faa2
fix links 2020-12-12 12:26:12 +03:00
Roman Storm 1ad2158af1
revert websnark cli 2020-10-22 09:50:15 -07:00
Roman Storm 18e6a19800
fix cli.js issue 2020-10-21 22:13:06 -07:00
poma 17308c9670
fix tests for node12 2020-07-31 19:54:24 +03:00
Roman Storm 5c3648cedc
Merge pull request #53 from gnidan/fix-weird-test-error
Fix strange failure to run tests the first time
2020-06-09 12:42:45 -07:00
Roman Storm a0ef1a526d
Merge pull request #55 from tornadocash/phase2
CLI and Phase2 updates
2020-06-09 12:39:27 -07:00
Roman Semenov fddd79d9bd
Merge pull request #56 from tsunamidev/patch-1
Update minimal demo instructions
2020-05-26 17:54:44 +03:00
Alexey 09baf9761d Update docs 2020-05-22 13:50:12 +03:00
Alexey ea1435b115 cli updates 2020-05-22 12:24:53 +03:00
Alexey 6d383235bb Merge branch 'master' into phase2 2020-05-22 11:40:15 +03:00
Alexey f48861a4f2 revert websnark version 2020-05-22 11:39:34 +03:00
Tsunami 4408f39b5a
Update minimal demo instructions
Slight issues in commands.
2020-05-16 15:09:46 +01:00
Alexey c7d912c2e7 cli compiance done 2020-05-15 13:59:38 +03:00
Alexey 4e120f26cb cli compliance WIP 2020-05-14 16:40:31 +03:00
Alexey f90a898001 fix PRIVATE_KEY dependency 2020-05-14 12:52:47 +03:00
Alexey 03175a2277 docs update 2020-05-14 12:49:56 +03:00
g. nicholas d'andrea 7ceebf48d5 Generate Hasher artifact upon compile, not migrate
To fix intermittent `truffle test` failures, use Truffle's external
compiler system for generating a valid artifact for Hasher at
compile-time instead of in migrations/2_deploy_hasher.js.

- Define compileHelper.js script, which outputs temporary artifact
  data to `./build/Hasher.json`.

- Configure `external` compiler in truffle-config to run compileHelper
  and process result

- Remove usage of @truffle/artifactor in migration, since the artifact
  will now already exist
2020-04-29 02:07:54 -04:00
Alexey 55e50fee3e websnark update 2020-04-13 22:03:04 +03:00
49 changed files with 14546 additions and 9343 deletions

27
.eslintrc Normal file
View File

@ -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 }]
}
}

View File

@ -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"
}
}

38
.github/workflows/build.yml vendored Normal file
View File

@ -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 }}

2
.gitignore vendored
View File

@ -96,3 +96,5 @@ typings/
ERC20Tornado_flat.sol
ETHTornado_flat.sol
coverage.json

2
.nvmrc
View File

@ -1 +1 @@
11
12

9
.prettierignore Normal file
View File

@ -0,0 +1,9 @@
.vscode
build
circuits
contracts/Verifier.sol
scripts/ganacheHelper.js
cli.js
index.js
coverage
coverage.json

16
.prettierrc Normal file
View File

@ -0,0 +1,16 @@
{
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"semi": false,
"printWidth": 110,
"overrides": [
{
"files": "*.sol",
"options": {
"singleQuote": false,
"printWidth": 130
}
}
]
}

View File

@ -1,6 +1,13 @@
{
"extends": "solhint:recommended",
"rules": {
"indent": ["error", 2]
}
"prettier/prettier": [
"error",
{
"printWidth": 110
}
],
"quotes": ["error", "double"]
},
"plugins": ["prettier"]
}

View File

@ -1,14 +0,0 @@
dist: trusty
language: node_js
node_js:
- "11"
install:
- npm ci
- cp .env.example .env
- travis_wait 30 npm run build
- npx ganache-cli > /dev/null &
- npm run migrate:dev
script:
- npm run test
- npm run eslint
- node cli.js test

107
README.md
View File

@ -1,44 +1,47 @@
# Tornado Cash Privacy Solution [![Build Status](https://travis-ci.org/tornadocash/tornado-core.svg?branch=master)](https://travis-ci.org/tornadocash/tornado-core)
# 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)
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 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.
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. In order to do that, the user should provide a proof that he or she possesses a secret to an unspent commitment from the smart contracts 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.
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 contracts 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)
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 const: 1088354 (43381 + 50859 * tree_depth)
- 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)
- Circuit Constraints = 28271 (1869 + 1325 \* tree_depth)
- Circuit Proof time = 10213ms (1071 + 347 \* tree_depth)
- Serverless
![image](diagram.png)
![image](docs/diagram.png)
## Whitepaper
**[https://tornado.cash/Tornado.cash_whitepaper_v1.4.pdf](https://tornado.cash/Tornado.cash_whitepaper_v1.4.pdf)**
**[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.
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:
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/Tornado_cryptographic_review.pdf
* Smart contract audit https://tornado.cash/Tornado_solidity_audit.pdf
* Zk-SNARK circuits audit https://tornado.cash/Tornado_circuit_audit.pdf
- 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
You can see example usage in cli.js, it works both in console and in browser.
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`
@ -53,13 +56,16 @@ Use browser version on Kovan:
1. `npx http-server` - serve current dir, you can use any other static http server
1. Open `localhost:8080`
Use with command line version. Works for Ganache, Kovan and Mainnet:
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`
@ -67,14 +73,16 @@ Use with command line version. Works for Ganache, Kovan and Mainnet:
1. `./cli.js --help`
### Kovan, Mainnet
1. make sure you complete steps from Initialization
1. Add `PRIVATE_KEY` to `.env` file
1. `./cli.js --help`
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
@ -86,39 +94,86 @@ Example:
./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
> Relay address: 0x6A31736e7490AbE5D5676be059DFf064AB4aC754
> Getting current state from tornado contract
> Generating SNARK proof
> Proof time: 9117.051ms
> Sending withdraw transaction through relay
> 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 4th or 5th migration for ETH or ERC20 contracts respectively (`--f 4 --to 4` or `--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 downloadKeys`
1. `npm build:contract`
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
```

140
config.js
View File

@ -1,140 +0,0 @@
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
}
}
}
}

View File

@ -1,74 +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
*/
* 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.17;
// 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 {
address public token;
using SafeERC20 for IERC20;
IERC20 public token;
constructor(
IVerifier _verifier,
IHasher _hasher,
uint256 _denomination,
uint32 _merkleTreeHeight,
address _operator,
address _token
) Tornado(_verifier, _denomination, _merkleTreeHeight, _operator) public {
IERC20 _token
) Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight) {
token = _token;
}
function _processDeposit() internal {
function _processDeposit() internal override {
require(msg.value == 0, "ETH value is supposed to be 0 for ERC20 instance");
_safeErc20TransferFrom(msg.sender, address(this), denomination);
token.safeTransferFrom(msg.sender, address(this), denomination);
}
function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) internal {
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");
_safeErc20Transfer(_recipient, denomination - _fee);
token.safeTransfer(_recipient, denomination - _fee);
if (_fee > 0) {
_safeErc20Transfer(_relayer, _fee);
token.safeTransfer(_relayer, _fee);
}
if (_refund > 0) {
(bool success, ) = _recipient.call.value(_refund)("");
(bool success, ) = _recipient.call{ value: _refund }("");
if (!success) {
// let's return _refund back to the relayer
_relayer.transfer(_refund);
}
}
}
function _safeErc20TransferFrom(address _from, address _to, uint256 _amount) internal {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd /* transferFrom */, _from, _to, _amount));
require(success, "not enough allowed tokens");
// if contract returns some data lets make sure that is `true` according to standard
if (data.length > 0) {
require(data.length == 32, "data length should be either 0 or 32 bytes");
success = abi.decode(data, (bool));
require(success, "not enough allowed tokens. Token returns false.");
}
}
function _safeErc20Transfer(address _to, uint256 _amount) internal {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb /* transfer */, _to, _amount));
require(success, "not enough tokens");
// if contract returns some data lets make sure that is `true` according to standard
if (data.length > 0) {
require(data.length == 32, "data length should be either 0 or 32 bytes");
success = abi.decode(data, (bool));
require(success, "not enough tokens. Token returns false.");
}
}
}

View File

@ -1,40 +1,45 @@
// 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.17;
// 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,
address _operator
) Tornado(_verifier, _denomination, _merkleTreeHeight, _operator) public {
}
uint32 _merkleTreeHeight
) Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight) {}
function _processDeposit() internal {
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 {
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)("");
(bool success, ) = _recipient.call{ value: denomination - _fee }("");
require(success, "payment to _recipient did not go thru");
if (_fee > 0) {
(success, ) = _relayer.call.value(_fee)("");
(success, ) = _relayer.call{ value: _fee }("");
require(success, "payment to _relayer did not go thru");
}
}

View File

@ -1,71 +1,74 @@
// 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.17;
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
library Hasher {
function MiMCSponge(uint256 in_xL, uint256 in_xR) 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 constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
uint256 public constant ZERO_VALUE = 21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE
IHasher public immutable hasher;
uint32 public levels;
// the following variables are made public for easier testing and debugging and
// are not supposed to be accessed in regular code
bytes32[] public filledSubtrees;
bytes32[] public zeros;
// 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;
uint32 public constant ROOT_HISTORY_SIZE = 100;
bytes32[ROOT_HISTORY_SIZE] public roots;
constructor(uint32 _treeLevels) public {
require(_treeLevels > 0, "_treeLevels should be greater than zero");
require(_treeLevels < 32, "_treeLevels should be less than 32");
levels = _treeLevels;
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;
bytes32 currentZero = bytes32(ZERO_VALUE);
zeros.push(currentZero);
filledSubtrees.push(currentZero);
for (uint32 i = 1; i < levels; i++) {
currentZero = hashLeftRight(currentZero, currentZero);
zeros.push(currentZero);
filledSubtrees.push(currentZero);
for (uint32 i = 0; i < _levels; i++) {
filledSubtrees[i] = zeros(i);
}
roots[0] = hashLeftRight(currentZero, currentZero);
roots[0] = zeros(_levels - 1);
}
/**
@dev Hash 2 tree leaves, returns MiMC(_left, _right)
*/
function hashLeftRight(bytes32 _left, bytes32 _right) public pure returns (bytes32) {
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, C) = Hasher.MiMCSponge(R, C);
(R, C) = _hasher.MiMCSponge(R, C);
R = addmod(R, uint256(_right), FIELD_SIZE);
(R, C) = Hasher.MiMCSponge(R, C);
(R, C) = _hasher.MiMCSponge(R, C);
return bytes32(R);
}
function _insert(bytes32 _leaf) internal returns(uint32 index) {
uint32 currentIndex = nextIndex;
require(currentIndex != uint32(2)**levels, "Merkle tree is full. No more leafs can be added");
nextIndex += 1;
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;
@ -73,32 +76,32 @@ contract MerkleTreeWithHistory {
for (uint32 i = 0; i < levels; i++) {
if (currentIndex % 2 == 0) {
left = currentLevelHash;
right = zeros[i];
right = zeros(i);
filledSubtrees[i] = currentLevelHash;
} else {
left = filledSubtrees[i];
right = currentLevelHash;
}
currentLevelHash = hashLeftRight(left, right);
currentLevelHash = hashLeftRight(hasher, left, right);
currentIndex /= 2;
}
currentRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
roots[currentRootIndex] = currentLevelHash;
return nextIndex - 1;
uint32 newRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
currentRootIndex = newRootIndex;
roots[newRootIndex] = currentLevelHash;
nextIndex = _nextIndex + 1;
return _nextIndex;
}
/**
@dev Whether the root is present in the root history
*/
function isKnownRoot(bytes32 _root) public view returns(bool) {
function isKnownRoot(bytes32 _root) public view returns (bool) {
if (_root == 0) {
return false;
}
uint32 i = currentRootIndex;
uint32 _currentRootIndex = currentRootIndex;
uint32 i = _currentRootIndex;
do {
if (_root == roots[i]) {
return true;
@ -107,14 +110,51 @@ contract MerkleTreeWithHistory {
i = ROOT_HISTORY_SIZE;
}
i--;
} while (i != currentRootIndex);
} while (i != _currentRootIndex);
return false;
}
/**
@dev Returns the last root
*/
function getLastRoot() public view returns(bytes32) {
function getLastRoot() public view returns (bytes32) {
return roots[currentRootIndex];
}
/// @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");
}
}

View File

@ -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);
}
}

View File

@ -1,7 +1,8 @@
pragma solidity ^0.5.0;
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract BadRecipient {
function() external {
fallback() external {
require(false, "this contract does not accept ETH");
}
}

View File

@ -1,10 +1,10 @@
pragma solidity ^0.5.0;
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Mintable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";
contract ERC20Mock is ERC20Detailed, ERC20Mintable {
constructor() ERC20Detailed("DAIMock", "DAIM", 18) public {
contract ERC20Mock is ERC20("DAIMock", "DAIM") {
function mint(address account, uint256 amount) public {
_mint(account, amount);
}
}

View File

@ -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);
}

View File

@ -1,20 +1,33 @@
pragma solidity 0.5.17;
// SPDX-License-Identifier: MIT
contract ERC20Basic {
uint public _totalSupply;
function totalSupply() public view returns (uint);
function balanceOf(address who) public view returns (uint);
function transfer(address to, uint value) public;
event Transfer(address indexed from, address indexed to, uint value);
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
*/
contract IUSDT is ERC20Basic {
function allowance(address owner, address spender) public view returns (uint);
function transferFrom(address from, address to, uint value) public;
function approve(address spender, uint value) public;
event Approval(address indexed owner, address indexed spender, uint value);
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);
}

View File

@ -1,12 +1,12 @@
pragma solidity 0.5.17;
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
import '../MerkleTreeWithHistory.sol';
import "../MerkleTreeWithHistory.sol";
contract MerkleTreeWithHistoryMock is MerkleTreeWithHistory {
constructor (uint32 _treeLevels) MerkleTreeWithHistory(_treeLevels) public {}
constructor(uint32 _treeLevels, IHasher _hasher) MerkleTreeWithHistory(_treeLevels, _hasher) {}
function insert(bytes32 _leaf) public {
_insert(_leaf);
_insert(_leaf);
}
}

View File

@ -1,37 +1,31 @@
// 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.17;
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
import "./MerkleTreeWithHistory.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract IVerifier {
function verifyProof(bytes memory _proof, uint256[6] memory _input) public returns(bool);
interface IVerifier {
function verifyProof(bytes memory _proof, uint256[6] memory _input) external returns (bool);
}
contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
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;
IVerifier public verifier;
// operator can update snark verification key
// after the final trusted setup ceremony operator rights are supposed to be transferred to zero address
address public operator;
modifier onlyOperator {
require(msg.sender == operator, "Only operator can call this function.");
_;
}
event Deposit(bytes32 indexed commitment, uint32 leafIndex, uint256 timestamp);
event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 fee);
@ -39,19 +33,18 @@ contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
/**
@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
@param _operator operator address (see operator comment above)
*/
constructor(
IVerifier _verifier,
IHasher _hasher,
uint256 _denomination,
uint32 _merkleTreeHeight,
address _operator
) MerkleTreeWithHistory(_merkleTreeHeight) public {
uint32 _merkleTreeHeight
) MerkleTreeWithHistory(_merkleTreeHeight, _hasher) {
require(_denomination > 0, "denomination should be greater than 0");
verifier = _verifier;
operator = _operator;
denomination = _denomination;
}
@ -70,7 +63,7 @@ contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
}
/** @dev this function is defined in a child contract */
function _processDeposit() internal;
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
@ -80,11 +73,25 @@ contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
- 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 {
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");
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);
@ -92,33 +99,25 @@ contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
}
/** @dev this function is defined in a child contract */
function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) internal;
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) {
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) {
function isSpentArray(bytes32[] calldata _nullifierHashes) external view returns (bool[] memory spent) {
spent = new bool[](_nullifierHashes.length);
for(uint i = 0; i < _nullifierHashes.length; i++) {
for (uint256 i = 0; i < _nullifierHashes.length; i++) {
if (isSpent(_nullifierHashes[i])) {
spent[i] = true;
}
}
}
/**
@dev allow operator to update SNARK verification keys. This is needed to update keys after the final trusted setup ceremony is held.
After that operator rights are supposed to be transferred to zero address
*/
function updateVerifier(address _newVerifier) external onlyOperator {
verifier = IVerifier(_newVerifier);
}
/** @dev operator can change his address */
function changeOperator(address _newOperator) external onlyOperator {
operator = _newOperator;
}
}

View File

@ -1 +0,0 @@
../build/circuits/Verifier.sol

231
contracts/Verifier.sol Normal file
View File

@ -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
);
}
}

39
contracts/cTornado.sol Normal file
View File

@ -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)));
}
}

25
deploy.js Normal file
View File

@ -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)

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

BIN
docs/enslookup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

BIN
docs/resolver.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -1,16 +1,17 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<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>
<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>

View File

@ -1,195 +0,0 @@
const jsStorage = require('./Storage')
const hasherImpl = require('./MiMC')
class MerkleTree {
constructor(n_levels, defaultElements, prefix, storage, hasher) {
this.prefix = prefix
this.storage = storage || new jsStorage()
this.hasher = hasher || new hasherImpl()
this.n_levels = n_levels
this.zero_values = []
this.totalElements = 0
let current_zero_value = '21663839004416932945382355908790599225266501822907911457504978515578255421292'
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)
}
}
index = Number(index)
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

View File

@ -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

View File

@ -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

View File

@ -1,9 +0,0 @@
/* global artifacts */
const Migrations = artifacts.require('Migrations')
module.exports = function(deployer) {
if(deployer.network === 'mainnet') {
return
}
deployer.deploy(Migrations)
}

View File

@ -1,21 +1,6 @@
/* global artifacts */
const path = require('path')
const Hasher = artifacts.require('Hasher')
const genContract = require('circomlib/src/mimcsponge_gencontract.js')
const Artifactor = require('@truffle/artifactor')
module.exports = function(deployer) {
return deployer.then( async () => {
const contractsDir = path.join(__dirname, '..', 'build/contracts')
let artifactor = new Artifactor(contractsDir)
let contractName = 'Hasher'
await artifactor.save({
contractName,
abi: genContract.abi,
unlinked_binary: genContract.createCode('mimcsponge', 220),
}).then(async () => {
const hasherContract = artifacts.require(contractName)
await deployer.deploy(hasherContract)
})
})
module.exports = async function (deployer) {
await deployer.deploy(Hasher)
}

View File

@ -1,6 +1,6 @@
/* global artifacts */
const Verifier = artifacts.require('Verifier')
module.exports = function(deployer) {
module.exports = function (deployer) {
deployer.deploy(Verifier)
}

View File

@ -2,16 +2,20 @@
require('dotenv').config({ path: '../.env' })
const ETHTornado = artifacts.require('ETHTornado')
const Verifier = artifacts.require('Verifier')
const hasherContract = artifacts.require('Hasher')
const Hasher = artifacts.require('Hasher')
module.exports = function(deployer, network, accounts) {
module.exports = function (deployer) {
return deployer.then(async () => {
const { MERKLE_TREE_HEIGHT, ETH_AMOUNT } = process.env
const verifier = await Verifier.deployed()
const hasherInstance = await hasherContract.deployed()
await ETHTornado.link(hasherContract, hasherInstance.address)
const tornado = await deployer.deploy(ETHTornado, verifier.address, ETH_AMOUNT, MERKLE_TREE_HEIGHT, accounts[0])
console.log('ETHTornado\'s address ', tornado.address)
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)
})
}

View File

@ -2,29 +2,27 @@
require('dotenv').config({ path: '../.env' })
const ERC20Tornado = artifacts.require('ERC20Tornado')
const Verifier = artifacts.require('Verifier')
const hasherContract = artifacts.require('Hasher')
const Hasher = artifacts.require('Hasher')
const ERC20Mock = artifacts.require('ERC20Mock')
module.exports = function(deployer, network, accounts) {
module.exports = function (deployer) {
return deployer.then(async () => {
const { MERKLE_TREE_HEIGHT, ERC20_TOKEN, TOKEN_AMOUNT } = process.env
const verifier = await Verifier.deployed()
const hasherInstance = await hasherContract.deployed()
await ERC20Tornado.link(hasherContract, hasherInstance.address)
const hasher = await Hasher.deployed()
let token = ERC20_TOKEN
if(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,
accounts[0],
token,
)
console.log('ERC20Tornado\'s address ', tornado.address)
console.log('ERC20Tornado address', tornado.address)
})
}

8130
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@
"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 cli.js -o index.js --exclude worker_threads",
"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",
@ -19,19 +19,23 @@
"migrate:kovan": "npx truffle migrate --network kovan --reset",
"migrate:rinkeby": "npx truffle migrate --network rinkeby --reset",
"migrate:mainnet": "npx truffle migrate --network mainnet",
"eslint": "npx eslint --ignore-path .gitignore .",
"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 downloadKeys.js"
"download": "node scripts/downloadKeys.js",
"coverage": "yarn truffle run coverage"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@openzeppelin/contracts": "^2.4.0",
"@truffle/artifactor": "^4.0.38",
"@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.5.0",
"chai": "^4.2.0",
@ -40,13 +44,21 @@
"circomlib": "git+https://github.com/tornadocash/circomlib.git#c372f14d324d57339c88451834bf2824e73bbdbc",
"commander": "^4.1.1",
"dotenv": "^8.2.0",
"eslint": "^6.6.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",
"truffle": "^5.0.44",
"solhint-plugin-prettier": "^0.0.5",
"truffle": "^5.1.67",
"truffle-flattener": "^1.4.2",
"web3": "^1.2.2",
"web3-utils": "^1.2.2",
"websnark": "git+https://github.com/tornadocash/websnark.git#2041cfa5fa0b71cd5cca9022a4eeea4afe28c9f7"
"web3": "^1.3.4",
"web3-utils": "^1.3.4",
"websnark": "git+https://github.com/tornadocash/websnark.git#4c0af6a8b65aabea3c09f377f63c44e7a58afa6d",
"solidity-coverage": "^0.7.20"
}
}

21
scripts/compileHasher.js Normal file
View File

@ -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()

View File

@ -2,8 +2,8 @@ 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 = 'build/circuits'
const contractsPath = 'build/contracts'
const circuitsPath = __dirname + '/../build/circuits'
const contractsPath = __dirname + '/../build/contracts'
async function downloadFile({ url, path }) {
const writer = fs.createWriteStream(path)
@ -11,7 +11,7 @@ async function downloadFile({ url, path }) {
const response = await axios({
url,
method: 'GET',
responseType: 'stream'
responseType: 'stream',
})
response.data.pipe(writer)
@ -25,16 +25,16 @@ async function downloadFile({ url, path }) {
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)){
if (!fs.existsSync(circuitsPath)) {
fs.mkdirSync(circuitsPath, { recursive: true })
fs.mkdirSync(contractsPath, { recursive: true })
}
for(let asset of assets) {
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)
path: path.resolve(__dirname, circuitsPath, asset.name),
})
}
}

View File

@ -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)
})
@ -48,5 +48,5 @@ module.exports = {
minerStop,
minerStart,
increaseTime,
traceTransaction
traceTransaction,
}

View File

@ -10,7 +10,7 @@ const snarkjs = require('snarkjs')
const crypto = require('crypto')
const circomlib = require('circomlib')
const bigInt = snarkjs.bigInt
const merkleTree = require('./lib/MerkleTree')
const merkleTree = require('fixed-merkle-tree')
const Web3 = require('web3')
const buildGroth16 = require('websnark/src/groth16')
const websnarkUtils = require('websnark/src/utils')
@ -24,7 +24,6 @@ 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
const networks = { '1': 'mainnet', '42': 'kovan' }
/** Generate random number of specified byte length */
const rbigint = nbytes => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
@ -45,7 +44,7 @@ async function printETHBalance({ address, name }) {
/** Display ERC20 account balance */
async function printERC20Balance({ address, name, tokenAddress }) {
const erc20ContractJson = require('./build/contracts/ERC20Mock.json')
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()))
}
@ -57,7 +56,9 @@ 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
}
@ -76,7 +77,7 @@ async function deposit({ currency, amount }) {
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 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
@ -84,7 +85,7 @@ async function deposit({ currency, amount }) {
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) {
if (isLocalRPC) {
console.log('Minting some test tokens to deposit')
await erc20.methods.mint(senderAccount, tokenAmount).send({ from: senderAccount, gas: 2e6 })
}
@ -93,11 +94,11 @@ async function deposit({ currency, amount }) {
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 })
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 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' })
}
@ -125,14 +126,16 @@ async function generateMerkleProof(deposit) {
const leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
// Validate that our data is correct
const isValidRoot = await tornado.methods.isKnownRoot(toHex(await tree.root())).call()
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
return tree.path(leafIndex)
const { pathElements, pathIndices } = tree.path(leafIndex)
return { pathElements, pathIndices, root: tree.root() }
}
/**
@ -145,7 +148,7 @@ async function generateMerkleProof(deposit) {
*/
async function generateProof({ deposit, recipient, relayerAddress = 0, fee = 0, refund = 0 }) {
// Compute merkle proof of our commitment
const { root, path_elements, path_index } = await generateMerkleProof(deposit)
const { root, pathElements, pathIndices } = await generateMerkleProof(deposit)
// Prepare circuit input
const input = {
@ -160,8 +163,8 @@ async function generateProof({ deposit, recipient, relayerAddress = 0, fee = 0,
// Private snark inputs
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
pathElements: pathElements,
pathIndices: pathIndices,
}
console.log('Generating SNARK proof')
@ -176,7 +179,7 @@ async function generateProof({ deposit, recipient, relayerAddress = 0, fee = 0,
toHex(input.recipient, 20),
toHex(input.relayer, 20),
toHex(input.fee),
toHex(input.refund)
toHex(input.refund),
]
return { proof, args }
@ -193,6 +196,9 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
}
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')
@ -206,17 +212,17 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
const { proof, args } = await generateProof({ deposit, recipient, relayerAddress, fee, refund })
console.log('Sending withdraw transaction through relay')
try{
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://${networks[netId]}.etherscan.io/tx/${relay.data.txHash}`)
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) {
} catch (e) {
if (e.response) {
console.error(e.response.data.error)
} else {
@ -228,13 +234,13 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
console.log('Submitting withdraw transaction')
await tornado.methods.withdraw(proof, ...args).send({ from: senderAccount, value: refund.toString(), gas: 1e6 })
.on('transactionHash', function(txHash){
.on('transactionHash', function (txHash) {
if (netId === 1 || netId === 42) {
console.log(`View transaction on etherscan https://${networks[netId]}.etherscan.io/tx/${txHash}`)
console.log(`View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${txHash}`)
} else {
console.log(`The transaction hash is ${txHash}`)
}
}).on('error', function(e){
}).on('error', function (e) {
console.error('on transactionHash error', e.message)
})
}
@ -260,7 +266,7 @@ function fromDecimals({ amount, decimals }) {
const comps = ether.split('.')
if (comps.length > 2) {
throw new Error(
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal points'
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal points',
)
}
@ -275,7 +281,7 @@ function fromDecimals({ amount, decimals }) {
}
if (fraction.length > baseLength) {
throw new Error(
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal places'
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal places',
)
}
@ -294,8 +300,58 @@ function fromDecimals({ amount, decimals }) {
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 feePercent = toBN(fromDecimals({ amount, decimals })).mul(toBN(relayerServiceFee * 10)).div(toBN('1000'))
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) {
@ -304,10 +360,9 @@ function calculateFee({ gasPrices, currency, amount, refund, ethPrices, relayerS
break
}
default: {
desiredFee =
expense.add(toBN(refund))
.mul(toBN(10 ** decimals))
.div(toBN(ethPrices[currency]))
desiredFee = expense.add(toBN(refund))
.mul(toBN(10 ** decimals))
.div(toBN(ethPrices[currency]))
desiredFee = desiredFee.add(feePercent)
break
}
@ -359,6 +414,60 @@ function parseNote(noteString) {
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
*/
@ -379,15 +488,23 @@ async function init({ rpc, noteNetId, currency = 'dai', amount = '100' }) {
} else {
// Initialize from local node
web3 = new Web3(rpc, null, { transactionConfirmationBlocks: 1 })
contractJson = require('./build/contracts/ETHTornado.json')
circuit = require('./build/circuits/withdraw.json')
proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT
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
erc20ContractJson = require('./build/contracts/ERC20Mock.json')
erc20tornadoJson = require('./build/contracts/ERC20Tornado.json')
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()
@ -403,22 +520,12 @@ async function init({ rpc, noteNetId, currency = 'dai', amount = '100' }) {
senderAccount = (await web3.eth.getAccounts())[0]
} else {
try {
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
senderAccount = account.address
} catch(e) {
console.error('Please provide PRIVATE_KEY in .env file')
process.exit(1)
}
try{
tornadoAddress = config.deployments[`netId${netId}`][currency].instanceAddress[amount]
if (!tornadoAddress) {
throw new Error()
}
tokenAddress = config.deployments[`netId${netId}`][currency].tokenAddress
} catch(e) {
} catch (e) {
console.error('There is no such tornado instance, check the currency and amount you provide')
process.exit(1)
}
@ -472,6 +579,34 @@ async function main() {
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.')
@ -489,13 +624,13 @@ async function main() {
amount = '100'
await init({ rpc: program.rpc, currency, amount })
noteString = await deposit({ currency, amount })
;(parsedNote = parseNote(noteString))
; (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) {
} catch (e) {
console.log('Error:', e)
process.exit(1)
}

140
src/config.js Normal file
View File

@ -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,
},
},
},
}

View File

@ -3,7 +3,7 @@ const assert = require('assert')
const { bigInt } = require('snarkjs')
const crypto = require('crypto')
const circomlib = require('circomlib')
const merkleTree = require('./lib/MerkleTree')
const merkleTree = require('fixed-merkle-tree')
const Web3 = require('web3')
const buildGroth16 = require('websnark/src/groth16')
const websnarkUtils = require('websnark/src/utils')
@ -18,13 +18,15 @@ const AMOUNT = '1'
// CURRENCY = 'ETH'
/** Generate random number of specified byte length */
const rbigint = nbytes => bigInt.leBuff2int(crypto.randomBytes(nbytes))
const rbigint = (nbytes) => bigInt.leBuff2int(crypto.randomBytes(nbytes))
/** Compute pedersen hash */
const pedersenHash = data => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
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')
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
@ -43,7 +45,9 @@ function createDeposit(nullifier, secret) {
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 })
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)}`
}
@ -87,22 +91,23 @@ async function generateMerkleProof(deposit) {
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)
.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 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(await tree.root())).call()
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
return await tree.path(leafIndex)
const { pathElements, pathIndices } = tree.path(leafIndex)
return { pathElements, pathIndices, root: tree.root() }
}
/**
@ -112,7 +117,7 @@ async function generateMerkleProof(deposit) {
*/
async function generateSnarkProof(deposit, recipient) {
// Compute merkle proof of our commitment
const { root, path_elements, path_index } = await generateMerkleProof(deposit)
const { root, pathElements, pathIndices } = await generateMerkleProof(deposit)
// Prepare circuit input
const input = {
@ -127,8 +132,8 @@ async function generateSnarkProof(deposit, recipient) {
// Private snark inputs
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
pathElements: pathElements,
pathIndices: pathIndices,
}
console.log('Generating SNARK proof...')
@ -141,19 +146,21 @@ async function generateSnarkProof(deposit, recipient) {
toHex(input.recipient, 20),
toHex(input.relayer, 20),
toHex(input.fee),
toHex(input.refund)
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('./build/circuits/withdraw.json')
proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
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)
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

View File

@ -1,12 +1,9 @@
/* global artifacts, web3, contract */
require('chai')
.use(require('bn-chai')(web3.utils.BN))
.use(require('chai-as-promised'))
.should()
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('../lib/ganacheHelper')
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
const Tornado = artifacts.require('./ERC20Tornado.sol')
const BadRecipient = artifacts.require('./BadRecipient.sol')
@ -21,11 +18,15 @@ const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt
const crypto = require('crypto')
const circomlib = require('circomlib')
const MerkleTree = require('../lib/MerkleTree')
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 toFixedHex = (number, length = 32) =>
'0x' +
bigInt(number)
.toString(16)
.padStart(length * 2, '0')
const getRandomRecipient = () => rbigint(20)
function generateDeposit() {
@ -38,7 +39,7 @@ function generateDeposit() {
return deposit
}
contract('ERC20Tornado', accounts => {
contract('ERC20Tornado', (accounts) => {
let tornado
let token
let usdtToken
@ -48,7 +49,6 @@ contract('ERC20Tornado', accounts => {
const levels = MERKLE_TREE_HEIGHT || 16
let tokenDenomination = TOKEN_AMOUNT || '1000000000000000000' // 1 ether
let snapshotId
let prefix = 'test'
let tree
const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
const refund = ETH_AMOUNT || '1000000000000000000' // 1 ether
@ -59,11 +59,7 @@ contract('ERC20Tornado', accounts => {
let proving_key
before(async () => {
tree = new MerkleTree(
levels,
null,
prefix,
)
tree = new MerkleTree(levels)
tornado = await Tornado.deployed()
if (ERC20_TOKEN) {
token = await Token.at(ERC20_TOKEN)
@ -111,7 +107,7 @@ contract('ERC20Tornado', accounts => {
it('should work', async () => {
const deposit = generateDeposit()
const user = accounts[4]
await tree.insert(deposit.commitment)
tree.insert(deposit.commitment)
await token.mint(user, tokenDenomination)
const balanceUserBefore = await token.balanceOf(user)
@ -124,11 +120,11 @@ contract('ERC20Tornado', accounts => {
const balanceUserAfter = await token.balanceOf(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
const { root, path_elements, path_index } = await tree.path(0)
const { pathElements, pathIndices } = tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root,
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer,
recipient,
@ -138,20 +134,19 @@ contract('ERC20Tornado', accounts => {
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
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 balanceRecieverBefore = await token.balanceOf(toFixedHex(recipient, 20))
const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20))
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
const ethBalanceRecieverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
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)
@ -164,23 +159,25 @@ contract('ERC20Tornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
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 balanceRecieverAfter = await token.balanceOf(toFixedHex(recipient, 20))
const ethBalanceRecieverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
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))
balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(tokenDenomination).sub(feeBN)))
balanceReceiverAfter.should.be.eq.BN(
toBN(balanceReceiverBefore).add(toBN(tokenDenomination).sub(feeBN)),
)
ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore))
ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).add(toBN(refund)))
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')
@ -195,7 +192,7 @@ contract('ERC20Tornado', accounts => {
const deposit = generateDeposit()
const user = accounts[4]
recipient = bigInt(badRecipient.address)
await tree.insert(deposit.commitment)
tree.insert(deposit.commitment)
await token.mint(user, tokenDenomination)
const balanceUserBefore = await token.balanceOf(user)
@ -205,11 +202,11 @@ contract('ERC20Tornado', accounts => {
const balanceUserAfter = await token.balanceOf(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
const { root, path_elements, path_index } = await tree.path(0)
const { pathElements, pathIndices } = tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root,
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer,
recipient,
@ -219,20 +216,19 @@ contract('ERC20Tornado', accounts => {
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
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 balanceRecieverBefore = await token.balanceOf(toFixedHex(recipient, 20))
const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20))
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
const ethBalanceRecieverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
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)
@ -243,23 +239,25 @@ contract('ERC20Tornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
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 balanceRecieverAfter = await token.balanceOf(toFixedHex(recipient, 20))
const ethBalanceRecieverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
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))
balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(tokenDenomination).sub(feeBN)))
balanceReceiverAfter.should.be.eq.BN(
toBN(balanceReceiverBefore).add(toBN(tokenDenomination).sub(feeBN)),
)
ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore))
ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore))
ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore))
ethBalanceRelayerAfter.should.be.eq.BN(toBN(ethBalanceRelayerBefore))
logs[0].event.should.be.equal('Withdrawal')
@ -273,17 +271,16 @@ contract('ERC20Tornado', accounts => {
it('should reject with wrong refund value', async () => {
const deposit = generateDeposit()
const user = accounts[4]
await tree.insert(deposit.commitment)
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 { root, path_elements, path_index } = await tree.path(0)
const { pathElements, pathIndices } = tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root,
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer,
recipient,
@ -293,11 +290,10 @@ contract('ERC20Tornado', accounts => {
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
@ -307,13 +303,16 @@ contract('ERC20Tornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
toFixedHex(input.refund),
]
let { reason } = await tornado.withdraw(proof, ...args, { value: 1, from: relayer, gasPrice: '0' }).should.be.rejected
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 } = 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')
})
@ -329,7 +328,7 @@ contract('ERC20Tornado', accounts => {
console.log('userBal', userBal.toString())
const senderBal = await usdtToken.balanceOf(sender)
console.log('senderBal', senderBal.toString())
await tree.insert(deposit.commitment)
tree.insert(deposit.commitment)
await usdtToken.transfer(user, tokenDenomination, { from: sender })
console.log('transfer done')
@ -345,12 +344,12 @@ contract('ERC20Tornado', accounts => {
const balanceUserAfter = await usdtToken.balanceOf(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
const { root, path_elements, path_index } = await tree.path(0)
const { pathElements, pathIndices } = tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root,
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer: operator,
recipient,
@ -360,19 +359,18 @@ contract('ERC20Tornado', accounts => {
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
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 balanceRecieverBefore = await usdtToken.balanceOf(toFixedHex(recipient, 20))
const ethBalanceRecieverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
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)
@ -385,22 +383,21 @@ contract('ERC20Tornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
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 balanceRecieverAfter = await usdtToken.balanceOf(toFixedHex(recipient, 20))
const ethBalanceRecieverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
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))
balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(tokenDenomination)))
ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).add(toBN(refund)).sub(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()))
@ -420,7 +417,7 @@ contract('ERC20Tornado', accounts => {
console.log('userBal', userBal.toString())
const senderBal = await token.balanceOf(sender)
console.log('senderBal', senderBal.toString())
await tree.insert(deposit.commitment)
tree.insert(deposit.commitment)
await token.transfer(user, tokenDenomination, { from: sender })
console.log('transfer done')
@ -434,12 +431,12 @@ contract('ERC20Tornado', accounts => {
const balanceUserAfter = await token.balanceOf(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
const { root, path_elements, path_index } = await tree.path(0)
const { pathElements, pathIndices } = tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root,
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer: operator,
recipient,
@ -449,19 +446,18 @@ contract('ERC20Tornado', accounts => {
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
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 balanceRecieverBefore = await token.balanceOf(toFixedHex(recipient, 20))
const ethBalanceRecieverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
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)
@ -474,7 +470,7 @@ contract('ERC20Tornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
toFixedHex(input.refund),
]
const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
console.log('withdraw done')
@ -482,15 +478,14 @@ contract('ERC20Tornado', accounts => {
const balanceTornadoAfter = await token.balanceOf(tornado.address)
const balanceRelayerAfter = await token.balanceOf(relayer)
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
const balanceRecieverAfter = await token.balanceOf(toFixedHex(recipient, 20))
const ethBalanceRecieverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
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))
balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(tokenDenomination)))
ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).add(toBN(refund)).sub(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()))
@ -505,10 +500,6 @@ contract('ERC20Tornado', accounts => {
await revertSnapshot(snapshotId.result)
// eslint-disable-next-line require-atomic-updates
snapshotId = await takeSnapshot()
tree = new MerkleTree(
levels,
null,
prefix,
)
tree = new MerkleTree(levels)
})
})

View File

@ -1,12 +1,9 @@
/* global artifacts, web3, contract */
require('chai')
.use(require('bn-chai')(web3.utils.BN))
.use(require('chai-as-promised'))
.should()
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('../lib/ganacheHelper')
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
const Tornado = artifacts.require('./ETHTornado.sol')
const { ETH_AMOUNT, MERKLE_TREE_HEIGHT } = process.env
@ -19,11 +16,15 @@ const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt
const crypto = require('crypto')
const circomlib = require('circomlib')
const MerkleTree = require('../lib/MerkleTree')
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 toFixedHex = (number, length = 32) =>
'0x' +
bigInt(number)
.toString(16)
.padStart(length * 2, '0')
const getRandomRecipient = () => rbigint(20)
function generateDeposit() {
@ -39,7 +40,7 @@ function generateDeposit() {
// eslint-disable-next-line no-unused-vars
function BNArrayToStringArray(array) {
const arrayToPrint = []
array.forEach(item => {
array.forEach((item) => {
arrayToPrint.push(item.toString())
})
return arrayToPrint
@ -51,14 +52,13 @@ function snarkVerify(proof) {
return snarkjs['groth'].isValid(verification_key, proof, proof.publicSignals)
}
contract('ETHTornado', accounts => {
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 prefix = 'test'
let tree
const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
const refund = bigInt(0)
@ -69,11 +69,7 @@ contract('ETHTornado', accounts => {
let proving_key
before(async () => {
tree = new MerkleTree(
levels,
null,
prefix,
)
tree = new MerkleTree(levels)
tornado = await Tornado.deployed()
snapshotId = await takeSnapshot()
groth16 = await buildGroth16()
@ -97,8 +93,8 @@ contract('ETHTornado', accounts => {
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] }))
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)
@ -116,11 +112,11 @@ contract('ETHTornado', accounts => {
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)
tree.insert(deposit.commitment)
const { pathElements, pathIndices } = tree.path(0)
const input = stringifyBigInts({
root,
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
@ -128,8 +124,8 @@ contract('ETHTornado', accounts => {
fee,
refund,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
pathElements: pathElements,
pathIndices: pathIndices,
})
let proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
@ -138,7 +134,8 @@ contract('ETHTornado', accounts => {
result.should.be.equal(true)
// nullifier
proofData.publicSignals[1] = '133792158246920651341275668520530514036799294649489851421007411546007850802'
proofData.publicSignals[1] =
'133792158246920651341275668520530514036799294649489851421007411546007850802'
result = snarkVerify(proofData)
result.should.be.equal(false)
proofData = originalProof
@ -161,7 +158,7 @@ contract('ETHTornado', accounts => {
it('should work', async () => {
const deposit = generateDeposit()
const user = accounts[4]
await tree.insert(deposit.commitment)
tree.insert(deposit.commitment)
const balanceUserBefore = await web3.eth.getBalance(user)
@ -173,12 +170,12 @@ contract('ETHTornado', accounts => {
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)
const { pathElements, pathIndices } = tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root,
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer: operator,
recipient,
@ -188,18 +185,17 @@ contract('ETHTornado', accounts => {
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
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 balanceRecieverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
const balanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
let isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(false)
@ -212,20 +208,19 @@ contract('ETHTornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
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 balanceRecieverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
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))
balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(value)).sub(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))
@ -237,13 +232,13 @@ contract('ETHTornado', accounts => {
it('should prevent double spend', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const { pathElements, pathIndices } = tree.path(0)
const input = stringifyBigInts({
root,
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
@ -251,8 +246,8 @@ contract('ETHTornado', accounts => {
fee,
refund,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
@ -262,7 +257,7 @@ contract('ETHTornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
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
@ -271,13 +266,13 @@ contract('ETHTornado', accounts => {
it('should prevent double spend with overflow', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const { pathElements, pathIndices } = tree.path(0)
const input = stringifyBigInts({
root,
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
@ -285,18 +280,22 @@ contract('ETHTornado', accounts => {
fee,
refund,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
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(
toBN(input.nullifierHash).add(
toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617'),
),
),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
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')
@ -304,13 +303,13 @@ contract('ETHTornado', accounts => {
it('fee should be less or equal transfer value', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const { pathElements, pathIndices } = tree.path(0)
const largeFee = bigInt(value).add(bigInt(1))
const input = stringifyBigInts({
root,
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
@ -318,8 +317,8 @@ contract('ETHTornado', accounts => {
fee: largeFee,
refund,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
@ -330,7 +329,7 @@ contract('ETHTornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
toFixedHex(input.refund),
]
const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Fee exceeds transfer value')
@ -338,22 +337,22 @@ contract('ETHTornado', accounts => {
it('should throw for corrupted merkle tree root', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const { pathElements, pathIndices } = tree.path(0)
const input = stringifyBigInts({
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
root,
root: tree.root(),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
@ -365,7 +364,7 @@ contract('ETHTornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
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')
@ -373,13 +372,13 @@ contract('ETHTornado', accounts => {
it('should reject with tampered public inputs', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
let { root, path_elements, path_index } = await tree.path(0)
let { pathElements, pathIndices } = tree.path(0)
const input = stringifyBigInts({
root,
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
@ -387,8 +386,8 @@ contract('ETHTornado', accounts => {
fee,
refund,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
let { proof } = websnarkUtils.toSolidityInput(proofData)
@ -398,7 +397,7 @@ contract('ETHTornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
toFixedHex(input.refund),
]
let incorrectArgs
const originalProof = proof.slice()
@ -410,7 +409,7 @@ contract('ETHTornado', accounts => {
toFixedHex('0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337', 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
toFixedHex(input.refund),
]
let error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof')
@ -422,7 +421,7 @@ contract('ETHTornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex('0x000000000000000000000000000000000000000000000000015345785d8a0000'),
toFixedHex(input.refund)
toFixedHex(input.refund),
]
error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof')
@ -434,7 +433,7 @@ contract('ETHTornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
toFixedHex(input.refund),
]
error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof')
@ -449,22 +448,22 @@ contract('ETHTornado', accounts => {
it('should reject with non zero refund', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const { pathElements, pathIndices } = tree.path(0)
const input = stringifyBigInts({
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
root,
root: tree.root(),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund: bigInt(1),
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
@ -476,74 +475,28 @@ contract('ETHTornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
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('#changeOperator', () => {
it('should work', async () => {
let operator = await tornado.operator()
operator.should.be.equal(sender)
const newOperator = accounts[7]
await tornado.changeOperator(newOperator).should.be.fulfilled
operator = await tornado.operator()
operator.should.be.equal(newOperator)
})
it('cannot change from different address', async () => {
let operator = await tornado.operator()
operator.should.be.equal(sender)
const newOperator = accounts[7]
const error = await tornado.changeOperator(newOperator, { from: accounts[7] }).should.be.rejected
error.reason.should.be.equal('Only operator can call this function.')
})
})
describe('#updateVerifier', () => {
it('should work', async () => {
let operator = await tornado.operator()
operator.should.be.equal(sender)
const newVerifier = accounts[7]
await tornado.updateVerifier(newVerifier).should.be.fulfilled
const verifier = await tornado.verifier()
verifier.should.be.equal(newVerifier)
})
it('cannot change from different address', async () => {
let operator = await tornado.operator()
operator.should.be.equal(sender)
const newVerifier = accounts[7]
const error = await tornado.updateVerifier(newVerifier, { from: accounts[7] }).should.be.rejected
error.reason.should.be.equal('Only operator can call this function.')
})
})
describe('#isSpent', () => {
it('should work', async () => {
const deposit1 = generateDeposit()
const deposit2 = generateDeposit()
await tree.insert(deposit1.commitment)
await tree.insert(deposit2.commitment)
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 { root, path_elements, path_index } = await tree.path(1)
const { pathElements, pathIndices } = tree.path(1)
// Circuit input
const input = stringifyBigInts({
// public
root,
root: tree.root(),
nullifierHash: pedersenHash(deposit2.nullifier.leInt2Buff(31)),
relayer: operator,
recipient,
@ -553,11 +506,10 @@ contract('ETHTornado', accounts => {
// private
nullifier: deposit2.nullifier,
secret: deposit2.secret,
pathElements: path_elements,
pathIndices: path_index,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
@ -567,7 +519,7 @@ contract('ETHTornado', accounts => {
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
toFixedHex(input.refund),
]
await tornado.withdraw(proof, ...args, { from: relayer, gasPrice: '0' })
@ -583,10 +535,6 @@ contract('ETHTornado', accounts => {
await revertSnapshot(snapshotId.result)
// eslint-disable-next-line require-atomic-updates
snapshotId = await takeSnapshot()
tree = new MerkleTree(
levels,
null,
prefix,
)
tree = new MerkleTree(levels)
})
})

View File

@ -1,16 +1,12 @@
/* 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 hasherContract = artifacts.require('./Hasher.sol')
const MerkleTree = require('../lib/MerkleTree')
const hasherImpl = require('../lib/MiMC')
const MerkleTree = require('fixed-merkle-tree')
const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt
@ -20,7 +16,7 @@ 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
@ -33,7 +29,7 @@ function toFixedHex(number, length = 32) {
return str
}
contract('MerkleTreeWithHistory', accounts => {
contract('MerkleTreeWithHistory', (accounts) => {
let merkleTreeWithHistory
let hasherInstance
let levels = MERKLE_TREE_HEIGHT || 16
@ -41,19 +37,12 @@ contract('MerkleTreeWithHistory', accounts => {
// eslint-disable-next-line no-unused-vars
const value = ETH_AMOUNT || '1000000000000000000'
let snapshotId
let prefix = 'test'
let tree
let hasher
before(async () => {
tree = new MerkleTree(
levels,
null,
prefix,
)
tree = new MerkleTree(levels)
hasherInstance = await hasherContract.deployed()
await MerkleTreeWithHistory.link(hasherContract, hasherInstance.address)
merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels)
merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, hasherInstance.address)
snapshotId = await takeSnapshot()
})
@ -67,132 +56,31 @@ contract('MerkleTreeWithHistory', accounts => {
})
})
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 hasherImpl()
tree = new MerkleTree(
2,
null,
prefix,
)
await tree.insert(toFixedHex('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,
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,
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,
elements,
prefix,
)
console.timeEnd('MerkleTree')
// 2,7 GHz Intel Core i7
// 1000 : 1949.084ms
// 10000: 19456.220ms
// 30000: 63406.679ms
})
})
describe('#insert', () => {
it('should insert', async () => {
let rootFromContract
for (let i = 1; i < 11; i++) {
await merkleTreeWithHistory.insert(toFixedHex(i), { from: sender })
await tree.insert(i)
let { root } = await tree.path(i - 1)
tree.insert(i)
rootFromContract = await merkleTreeWithHistory.getLastRoot()
toFixedHex(root).should.be.equal(rootFromContract.toString())
toFixedHex(tree.root()).should.be.equal(rootFromContract.toString())
}
})
it('should reject if tree is full', async () => {
const levels = 6
const merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels)
const merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, hasherInstance.address)
for (let i = 0; i < 2**levels; i++) {
await merkleTreeWithHistory.insert(toFixedHex(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(toFixedHex(1337)).should.be.rejected
error.reason.should.be.equal('Merkle tree is full. No more leafs can be added')
error.reason.should.be.equal('Merkle tree is full. No more leaves can be added')
error = await merkleTreeWithHistory.insert(toFixedHex(1)).should.be.rejected
error.reason.should.be.equal('Merkle tree is full. No more leafs can be added')
error.reason.should.be.equal('Merkle tree is full. No more leaves can be added')
})
it.skip('hasher gas', async () => {
@ -207,19 +95,16 @@ contract('MerkleTreeWithHistory', accounts => {
describe('#isKnownRoot', () => {
it('should work', async () => {
let path
for (let i = 1; i < 5; i++) {
await merkleTreeWithHistory.insert(toFixedHex(i), { from: sender }).should.be.fulfilled
await tree.insert(i)
path = await tree.path(i - 1)
let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(path.root))
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(path.root))
let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(tree.root()))
isKnown.should.be.equal(true)
})
@ -230,18 +115,10 @@ contract('MerkleTreeWithHistory', accounts => {
})
})
afterEach(async () => {
await revertSnapshot(snapshotId.result)
// eslint-disable-next-line require-atomic-updates
snapshotId = await takeSnapshot()
hasher = new hasherImpl()
tree = new MerkleTree(
levels,
null,
prefix,
null,
hasher,
)
tree = new MerkleTree(levels)
})
})

View File

@ -1,10 +1,6 @@
require('dotenv').config()
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,47 +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/97c8bf358b9942a9853fab1ba93dc5b3'),
provider: () =>
new HDWalletProvider(
process.env.PRIVATE_KEY,
'https://kovan.infura.io/v3/97c8bf358b9942a9853fab1ba93dc5b3',
),
network_id: 42,
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'),
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
skipDryRun: true,
},
mainnet: {
provider: () => new HDWalletProvider(process.env.PRIVATE_KEY, 'http://ethereum-rpc.trustwalletapp.com'),
@ -67,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
},
@ -86,15 +76,23 @@ module.exports = {
// Configure your compilers
compilers: {
solc: {
version: '0.5.17', // 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'],
}

13191
yarn.lock Normal file

File diff suppressed because it is too large Load Diff