Compare commits
69 Commits
Author | SHA1 | Date |
---|---|---|
Alexey Pertsev | 1ef6a263ac | |
Drygin | f9f19b70e4 | |
Drygin | 10aeb05417 | |
Roman Semenov | 896fc224ff | |
HowJMay | 0b8bbf6317 | |
poma | 7d924e2447 | |
poma | 2ec693d0de | |
poma | 0cf811b854 | |
poma | f468de9a0a | |
Roman Semenov | 94dfad9cd2 | |
poma | 23543683f3 | |
poma | 0c6e638852 | |
poma | 801f29a4b7 | |
mirru2532 | 5cb9d60178 | |
Alexey | 3603b1c9e1 | |
Alexey | c12643e2c2 | |
poma | 54a7bdcb04 | |
poma | f189a657c9 | |
poma | 3ad634594e | |
Alexey | 127a61e21f | |
Alexey | c559a79396 | |
Alexey | 78bd4175fa | |
Alexey | 4069b61421 | |
poma | f5d8f6d971 | |
poma | 8580c5e427 | |
poma | a359e86f85 | |
poma | 346ffcee3c | |
poma | c6b442713a | |
Roman Storm | 3c4def1e64 | |
Roman Semenov | 77af0c5bdd | |
Lucien Nocelli | 4f0a23426f | |
poma | b438f8db7b | |
Alexey Pertsev | d02ff4faa2 | |
Roman Storm | 1ad2158af1 | |
Roman Storm | 18e6a19800 | |
poma | 17308c9670 | |
Roman Storm | 5c3648cedc | |
Roman Storm | a0ef1a526d | |
Roman Semenov | fddd79d9bd | |
Alexey | 09baf9761d | |
Alexey | ea1435b115 | |
Alexey | 6d383235bb | |
Alexey | f48861a4f2 | |
Tsunami | 4408f39b5a | |
Alexey | c7d912c2e7 | |
Alexey | 4e120f26cb | |
Alexey | f90a898001 | |
Alexey | 03175a2277 | |
g. nicholas d'andrea | 7ceebf48d5 | |
Alexey | a533ad9ffb | |
poma | d4e6031982 | |
Alexey | 2d9677831a | |
Alexey | f04ff2b6fd | |
Alexey | 55e50fee3e | |
Alexey | b0bca7fc36 | |
Alexey | 54bb4c4b3c | |
Alexey | 411098b589 | |
Alexey | c8adb6b200 | |
Alexey | cfaf325c47 | |
Pertsev Alexey | 30b07f76a5 | |
Pertsev Alexey | d1c4a9bee6 | |
Alexey | 6a592154d2 | |
poma | 49bdd1bb6f | |
Alexey | 0e0ac72b0f | |
Alexey | 3e9df20f35 | |
Alexey | 62c7951961 | |
Alexey | 8afd208765 | |
Alexey | ff71072700 | |
Alexey | 27316a1edd |
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "prettier"],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"require-await": "error",
|
||||
"prettier/prettier": ["error", { "printWidth": 110 }]
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"require-await": "error"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['*']
|
||||
tags: ['v[0-9]+.[0-9]+.[0-9]+']
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- run: yarn install
|
||||
- run: yarn download
|
||||
- run: cp .env.example .env
|
||||
- run: npx ganache-cli > /dev/null &
|
||||
- run: npm run migrate:dev
|
||||
- run: yarn test
|
||||
- run: node src/cli.js test
|
||||
- run: yarn lint
|
||||
- run: yarn coverage
|
||||
- name: Coveralls
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Telegram Failure Notification
|
||||
uses: appleboy/telegram-action@0.0.7
|
||||
if: failure()
|
||||
with:
|
||||
message: ❗ Build failed for [${{ github.repository }}](https://github.com/${{ github.repository }}/actions) because of ${{ github.actor }}
|
||||
format: markdown
|
||||
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
@ -96,3 +96,5 @@ typings/
|
|||
|
||||
ERC20Tornado_flat.sol
|
||||
ETHTornado_flat.sol
|
||||
|
||||
coverage.json
|
|
@ -0,0 +1,9 @@
|
|||
.vscode
|
||||
build
|
||||
circuits
|
||||
contracts/Verifier.sol
|
||||
scripts/ganacheHelper.js
|
||||
cli.js
|
||||
index.js
|
||||
coverage
|
||||
coverage.json
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"semi": false,
|
||||
"printWidth": 110,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.sol",
|
||||
"options": {
|
||||
"singleQuote": false,
|
||||
"printWidth": 130
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,6 +1,13 @@
|
|||
{
|
||||
"extends": "solhint:recommended",
|
||||
"rules": {
|
||||
"indent": ["error", 2]
|
||||
}
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"printWidth": 110
|
||||
}
|
||||
],
|
||||
"quotes": ["error", "double"]
|
||||
},
|
||||
"plugins": ["prettier"]
|
||||
}
|
||||
|
|
14
.travis.yml
14
.travis.yml
|
@ -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
|
||||
- ./cli.js test
|
150
README.md
150
README.md
|
@ -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 contract’s list of deposits. zkSnark technology allows that to happen without revealing which exact deposit corresponds to this secret. The smart contract will check the proof, and transfer deposited funds to the address specified for withdrawal. An external observer will be unable to determine which deposit this withdrawal came from.
|
||||
Later, the user decides to make a withdrawal. To do that, the user should provide a proof that he or she possesses a secret to an unspent commitment from the smart contract’s list of deposits. zkSnark technology allows that to happen without revealing which exact deposit corresponds to this secret. The smart contract will check the proof and transfer deposited funds to the address specified for withdrawal. An external observer will be unable to determine which deposit this withdrawal came from.
|
||||
|
||||
You can read more about it in [this medium article](https://medium.com/@tornado.cash/introducing-private-transactions-on-ethereum-now-42ee915babe0)
|
||||
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,35 +56,124 @@ 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 with Ganache:
|
||||
### ETHTornado
|
||||
1. `npm run migrate:dev`
|
||||
1. `./cli.js deposit`
|
||||
1. `./cli.js withdraw <note from previous step> <destination eth address>`
|
||||
1. `./cli.js balance <destination eth address>`
|
||||
Use the command-line version. Works for Ganache, Kovan, and Mainnet:
|
||||
|
||||
### ERC20Tornado
|
||||
1. `npm run migrate:dev`
|
||||
1. `./cli.js depositErc20`
|
||||
1. `./cli.js withdrawErc20 <note from previous step> <destination eth address> <relayer eth address>`
|
||||
1. `./cli.js balanceErc20 <destination eth address> <relayer eth address>`
|
||||
### Initialization
|
||||
|
||||
If you want, you can point the app to existing tornado contracts on Mainnet or Kovan. It should work without any problems
|
||||
1. `cp .env.example .env`
|
||||
1. `npm run download`
|
||||
1. `npm run build:contract`
|
||||
|
||||
### Ganache
|
||||
|
||||
1. make sure you complete steps from Initialization
|
||||
1. `ganache-cli -i 1337`
|
||||
1. `npm run migrate:dev`
|
||||
1. `./cli.js test`
|
||||
1. `./cli.js --help`
|
||||
|
||||
### Kovan, Mainnet
|
||||
|
||||
1. Please use https://github.com/tornadocash/tornado-cli
|
||||
Reason: because tornado-core uses websnark `2041cfa5fa0b71cd5cca9022a4eeea4afe28c9f7` commit hash in order to work with local trusted setup. Tornado-cli uses `4c0af6a8b65aabea3c09f377f63c44e7a58afa6d` commit with production trusted setup of tornadoCash
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
./cli.js deposit ETH 0.1 --rpc https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448
|
||||
```
|
||||
|
||||
> Your note: tornado-eth-0.1-42-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652
|
||||
> Tornado ETH balance is 8.9
|
||||
> Sender account ETH balance is 1004873.470619891361352542
|
||||
> Submitting deposit transaction
|
||||
> Tornado ETH balance is 9
|
||||
> Sender account ETH balance is 1004873.361652048361352542
|
||||
|
||||
```bash
|
||||
./cli.js withdraw tornado-eth-0.1-42-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 0x8589427373D6D84E98730D7795D8f6f8731FDA16 --rpc https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448 --relayer https://kovan-frelay.duckdns.org
|
||||
```
|
||||
|
||||
> Relay address: 0x6A31736e7490AbE5D5676be059DFf064AB4aC754
|
||||
> Getting current state from tornado contract
|
||||
> Generating SNARK proof
|
||||
> Proof time: 9117.051ms
|
||||
> Sending withdraw transaction through the relay
|
||||
> Transaction submitted through the relay. View transaction on etherscan https://kovan.etherscan.io/tx/0xcb21ae8cad723818c6bc7273e83e00c8393fcdbe74802ce5d562acad691a2a7b
|
||||
> Transaction mined in block 17036120
|
||||
> Done
|
||||
|
||||
## Deploy ETH Tornado Cash
|
||||
|
||||
1. `cp .env.example .env`
|
||||
1. Tune all necessary params
|
||||
1. `npx truffle migrate --network kovan --reset --f 2 --to 4`
|
||||
|
||||
## Deploy ERC20 Tornado Cash
|
||||
|
||||
1. `cp .env.example .env`
|
||||
1. Tune all necessary params
|
||||
1. `npx truffle migrate --network kovan --reset --f 2 --to 3`
|
||||
1. `npx truffle migrate --network kovan --reset --f 5`
|
||||
|
||||
**Note**. If you want to reuse the same verifier for all the instances, then after you deployed one of the instances you should only run 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 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
|
||||
```
|
||||
|
|
|
@ -50,6 +50,18 @@ template Withdraw(levels) {
|
|||
tree.pathElements[i] <== pathElements[i];
|
||||
tree.pathIndices[i] <== pathIndices[i];
|
||||
}
|
||||
|
||||
// Add hidden signals to make sure that tampering with recipient or fee will invalidate the snark proof
|
||||
// Most likely it is not required, but it's better to stay on the safe side and it only takes 2 constraints
|
||||
// Squares are used to prevent optimizer from removing those constraints
|
||||
signal recipientSquare;
|
||||
signal feeSquare;
|
||||
signal relayerSquare;
|
||||
signal refundSquare;
|
||||
recipientSquare <== recipient * recipient;
|
||||
feeSquare <== fee * fee;
|
||||
relayerSquare <== relayer * relayer;
|
||||
refundSquare <== refund * refund;
|
||||
}
|
||||
|
||||
component main = Withdraw(20);
|
||||
|
|
465
cli.js
465
cli.js
|
@ -1,465 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
// Temporary demo client
|
||||
// Works both in browser and node.js
|
||||
const fs = require('fs')
|
||||
const axios = require('axios')
|
||||
const assert = require('assert')
|
||||
const snarkjs = require('snarkjs')
|
||||
const crypto = require('crypto')
|
||||
const circomlib = require('circomlib')
|
||||
const bigInt = snarkjs.bigInt
|
||||
const merkleTree = require('./lib/MerkleTree')
|
||||
const Web3 = require('web3')
|
||||
const buildGroth16 = require('websnark/src/groth16')
|
||||
const websnarkUtils = require('websnark/src/utils')
|
||||
const { toWei, fromWei } = require('web3-utils')
|
||||
|
||||
let web3, tornado, erc20tornado, circuit, proving_key, groth16, erc20, senderAccount
|
||||
let MERKLE_TREE_HEIGHT, ETH_AMOUNT, TOKEN_AMOUNT, ERC20_TOKEN
|
||||
|
||||
/** Whether we are in a browser or node.js */
|
||||
const inBrowser = (typeof window !== 'undefined')
|
||||
|
||||
/** Generate random number of specified byte length */
|
||||
const rbigint = nbytes => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
|
||||
|
||||
/** Compute pedersen hash */
|
||||
const pedersenHash = data => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
|
||||
|
||||
/** BigNumber to hex string of specified length */
|
||||
function toHex(number, length = 32) {
|
||||
let str = number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)
|
||||
return '0x' + str.padStart(length * 2, '0')
|
||||
}
|
||||
|
||||
/** Display account balance */
|
||||
async function printBalance(account, name) {
|
||||
console.log(`${name} ETH balance is`, web3.utils.fromWei(await web3.eth.getBalance(account)))
|
||||
console.log(`${name} Token Balance is`, web3.utils.fromWei(await erc20.methods.balanceOf(account).call()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create deposit object from secret and nullifier
|
||||
*/
|
||||
function createDeposit(nullifier, secret) {
|
||||
let deposit = { nullifier, secret }
|
||||
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
|
||||
deposit.commitment = pedersenHash(deposit.preimage)
|
||||
deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
|
||||
return deposit
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an ETH deposit
|
||||
*/
|
||||
async function deposit() {
|
||||
const deposit = createDeposit(rbigint(31), rbigint(31))
|
||||
|
||||
console.log('Submitting deposit transaction')
|
||||
await tornado.methods.deposit(toHex(deposit.commitment)).send({ value: ETH_AMOUNT, from: senderAccount, gas:2e6 })
|
||||
|
||||
const note = toHex(deposit.preimage, 62)
|
||||
console.log('Your note:', note)
|
||||
return note
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an ERC20 deposit
|
||||
*/
|
||||
async function depositErc20() {
|
||||
const deposit = createDeposit(rbigint(31), rbigint(31))
|
||||
|
||||
if(ERC20_TOKEN === '') {
|
||||
console.log('Minting some test tokens to deposit')
|
||||
await erc20.methods.mint(senderAccount, TOKEN_AMOUNT).send({ from: senderAccount, gas: 2e6 })
|
||||
}
|
||||
|
||||
console.log('Approving tokens for deposit')
|
||||
await erc20.methods.approve(erc20tornado._address, TOKEN_AMOUNT).send({ from: senderAccount, gas:1e6 })
|
||||
|
||||
console.log('Submitting deposit transaction')
|
||||
await erc20tornado.methods.deposit(toHex(deposit.commitment)).send({ from: senderAccount, gas:2e6 })
|
||||
|
||||
const note = toHex(deposit.preimage, 62)
|
||||
console.log('Your note:', note)
|
||||
return note
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate merkle tree for a deposit.
|
||||
* Download deposit events from the contract, reconstructs merkle tree, finds our deposit leaf
|
||||
* in it and generates merkle proof
|
||||
* @param contract Tornado contract address
|
||||
* @param deposit Deposit object
|
||||
*/
|
||||
async function generateMerkleProof(contract, deposit) {
|
||||
// Get all deposit events from smart contract and assemble merkle tree from them
|
||||
console.log('Getting current state from tornado contract')
|
||||
const events = await contract.getPastEvents('Deposit', { fromBlock: contract.deployedBlock, toBlock: 'latest' })
|
||||
const leaves = events
|
||||
.sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order
|
||||
.map(e => e.returnValues.commitment)
|
||||
const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves)
|
||||
|
||||
// Find current commitment in the tree
|
||||
let depositEvent = events.find(e => e.returnValues.commitment === toHex(deposit.commitment))
|
||||
let leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
|
||||
|
||||
// Validate that our data is correct
|
||||
const isValidRoot = await contract.methods.isKnownRoot(toHex(await 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SNARK proof for withdrawal
|
||||
* @param contract Tornado contract address
|
||||
* @param note Note string
|
||||
* @param recipient Funds recipient
|
||||
* @param relayer Relayer address
|
||||
* @param fee Relayer fee
|
||||
* @param refund Receive ether for exchanged tokens
|
||||
*/
|
||||
async function generateProof(contract, note, recipient, relayer = 0, fee = 0, refund = 0) {
|
||||
// Decode hex string and restore the deposit object
|
||||
let buf = Buffer.from(note.slice(2), 'hex')
|
||||
let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62)))
|
||||
|
||||
// Compute merkle proof of our commitment
|
||||
const { root, path_elements, path_index } = await generateMerkleProof(contract, deposit)
|
||||
|
||||
// Prepare circuit input
|
||||
const input = {
|
||||
// Public snark inputs
|
||||
root: root,
|
||||
nullifierHash: deposit.nullifierHash,
|
||||
recipient: bigInt(recipient),
|
||||
relayer: bigInt(relayer),
|
||||
fee: bigInt(fee),
|
||||
refund: bigInt(refund),
|
||||
|
||||
// Private snark inputs
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: path_elements,
|
||||
pathIndices: path_index,
|
||||
}
|
||||
|
||||
console.log('Generating SNARK proof')
|
||||
console.time('Proof time')
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
console.timeEnd('Proof time')
|
||||
|
||||
const args = [
|
||||
toHex(input.root),
|
||||
toHex(input.nullifierHash),
|
||||
toHex(input.recipient, 20),
|
||||
toHex(input.relayer, 20),
|
||||
toHex(input.fee),
|
||||
toHex(input.refund)
|
||||
]
|
||||
|
||||
return { proof, args }
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an ETH withdrawal
|
||||
* @param note Note to withdraw
|
||||
* @param recipient Recipient address
|
||||
*/
|
||||
async function withdraw(note, recipient) {
|
||||
const { proof, args } = await generateProof(tornado, note, recipient)
|
||||
|
||||
console.log('Submitting withdraw transaction')
|
||||
await tornado.methods.withdraw(proof, ...args).send({ from: senderAccount, gas: 1e6 })
|
||||
console.log('Done')
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a ERC20 withdrawal
|
||||
* @param note Note to withdraw
|
||||
* @param recipient Recipient address
|
||||
*/
|
||||
async function withdrawErc20(note, recipient) {
|
||||
const { proof, args } = await generateProof(erc20tornado, note, recipient)
|
||||
|
||||
console.log('Submitting withdraw transaction')
|
||||
await erc20tornado.methods.withdraw(proof, ...args).send({ from: senderAccount, gas: 1e6 })
|
||||
console.log('Done')
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an ETH withdrawal through relay
|
||||
* @param note Note to withdraw
|
||||
* @param recipient Recipient address
|
||||
* @param relayUrl Relay url address
|
||||
*/
|
||||
async function withdrawRelay(note, recipient, relayUrl) {
|
||||
const resp = await axios.get(relayUrl + '/status')
|
||||
const { relayerAddress, netId, gasPrices } = resp.data
|
||||
assert(netId === await web3.eth.net.getId() || netId === '*', 'This relay is for different network')
|
||||
console.log('Relay address: ', relayerAddress)
|
||||
|
||||
const fee = bigInt(toWei(gasPrices.fast.toString(), 'gwei')).mul(bigInt(1e6))
|
||||
const { proof, args } = await generateProof(tornado, note, recipient, relayerAddress, fee)
|
||||
|
||||
console.log('Sending withdraw transaction through relay')
|
||||
const resp2 = await axios.post(relayUrl + '/relay', { contract: tornado._address, proof: { proof, publicSignals: args } })
|
||||
console.log(`Transaction submitted through relay, tx hash: ${resp2.data.txHash}`)
|
||||
|
||||
let receipt = await waitForTxReceipt(resp2.data.txHash)
|
||||
console.log('Transaction mined in block', receipt.blockNumber)
|
||||
console.log('Done')
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a ERC20 withdrawal through relay
|
||||
* @param note Note to withdraw
|
||||
* @param recipient Recipient address
|
||||
* @param relayUrl Relay url address
|
||||
*/
|
||||
async function withdrawRelayErc20(note, recipient, relayUrl) {
|
||||
const resp = await axios.get(relayUrl + '/status')
|
||||
const { relayerAddress, netId, gasPrices, ethPriceInDai } = resp.data
|
||||
assert(netId === await web3.eth.net.getId() || netId === '*', 'This relay is for different network')
|
||||
console.log('Relay address: ', relayerAddress)
|
||||
|
||||
const refund = bigInt(toWei('0.001'))
|
||||
const fee = bigInt(toWei(gasPrices.fast.toString(), 'gwei')).mul(bigInt(1e6)).add(refund).mul(bigInt(fromWei(ethPriceInDai.toString())))
|
||||
const { proof, args } = await generateProof(erc20tornado, note, recipient, relayerAddress, fee, refund)
|
||||
|
||||
console.log('Sending withdraw transaction through relay')
|
||||
const resp2 = await axios.post(relayUrl + '/relay', { contract: erc20tornado._address, proof: { proof, publicSignals: args } })
|
||||
console.log(`Transaction submitted through relay, tx hash: ${resp2.data.txHash}`)
|
||||
|
||||
let receipt = await waitForTxReceipt(resp2.data.txHash)
|
||||
console.log('Transaction mined in block', receipt.blockNumber)
|
||||
console.log('Done')
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for transaction to be mined
|
||||
* @param txHash Hash of transaction
|
||||
* @param attempts
|
||||
* @param delay
|
||||
*/
|
||||
function waitForTxReceipt(txHash, attempts = 60, delay = 1000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkForTx = async (txHash, retryAttempt = 0) => {
|
||||
const result = await web3.eth.getTransactionReceipt(txHash)
|
||||
if (!result || !result.blockNumber) {
|
||||
if (retryAttempt <= attempts) {
|
||||
setTimeout(() => checkForTx(txHash, retryAttempt + 1), delay)
|
||||
} else {
|
||||
reject(new Error('tx was not mined'))
|
||||
}
|
||||
} else {
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
checkForTx(txHash)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Init web3, contracts, and snark
|
||||
*/
|
||||
async function init() {
|
||||
let contractJson, erc20ContractJson, erc20tornadoJson
|
||||
if (inBrowser) {
|
||||
// Initialize using injected web3 (Metamask)
|
||||
// To assemble web version run `npm run browserify`
|
||||
web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 })
|
||||
contractJson = await (await fetch('build/contracts/ETHTornado.json')).json()
|
||||
circuit = await (await fetch('build/circuits/withdraw.json')).json()
|
||||
proving_key = await (await fetch('build/circuits/withdraw_proving_key.bin')).arrayBuffer()
|
||||
MERKLE_TREE_HEIGHT = 16
|
||||
ETH_AMOUNT = 1e18
|
||||
TOKEN_AMOUNT = 1e19
|
||||
} else {
|
||||
// Initialize from local node
|
||||
web3 = new Web3('http://localhost:8545', 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
|
||||
require('dotenv').config()
|
||||
MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT
|
||||
ETH_AMOUNT = process.env.ETH_AMOUNT
|
||||
TOKEN_AMOUNT = process.env.TOKEN_AMOUNT
|
||||
ERC20_TOKEN = process.env.ERC20_TOKEN
|
||||
erc20ContractJson = require('./build/contracts/ERC20Mock.json')
|
||||
erc20tornadoJson = require('./build/contracts/ERC20Tornado.json')
|
||||
}
|
||||
groth16 = await buildGroth16()
|
||||
let netId = await web3.eth.net.getId()
|
||||
if (contractJson.networks[netId]) {
|
||||
const tx = await web3.eth.getTransaction(contractJson.networks[netId].transactionHash)
|
||||
tornado = new web3.eth.Contract(contractJson.abi, contractJson.networks[netId].address)
|
||||
tornado.deployedBlock = tx.blockNumber
|
||||
}
|
||||
|
||||
const tx3 = await web3.eth.getTransaction(erc20tornadoJson.networks[netId].transactionHash)
|
||||
erc20tornado = new web3.eth.Contract(erc20tornadoJson.abi, erc20tornadoJson.networks[netId].address)
|
||||
erc20tornado.deployedBlock = tx3.blockNumber
|
||||
|
||||
if(ERC20_TOKEN === '') {
|
||||
erc20 = new web3.eth.Contract(erc20ContractJson.abi, erc20ContractJson.networks[netId].address)
|
||||
const tx2 = await web3.eth.getTransaction(erc20ContractJson.networks[netId].transactionHash)
|
||||
erc20.deployedBlock = tx2.blockNumber
|
||||
}
|
||||
|
||||
senderAccount = (await web3.eth.getAccounts())[0]
|
||||
console.log('Loaded')
|
||||
}
|
||||
|
||||
// ========== CLI related stuff below ==============
|
||||
|
||||
/** Print command line help */
|
||||
function printHelp(code = 0) {
|
||||
console.log(`Usage:
|
||||
Submit a deposit from default eth account and return the resulting note
|
||||
$ ./cli.js deposit
|
||||
$ ./cli.js depositErc20
|
||||
|
||||
Withdraw a note to 'recipient' account
|
||||
$ ./cli.js withdraw <note> <recipient> [relayUrl]
|
||||
$ ./cli.js withdrawErc20 <note> <recipient> [relayUrl]
|
||||
|
||||
Check address balance
|
||||
$ ./cli.js balance <address>
|
||||
|
||||
Perform an automated test
|
||||
$ ./cli.js test
|
||||
$ ./cli.js testRelay
|
||||
|
||||
Example:
|
||||
$ ./cli.js deposit
|
||||
...
|
||||
Your note: 0x1941fa999e2b4bfeec3ce53c2440c3bc991b1b84c9bb650ea19f8331baf621001e696487e2a2ee54541fa12f49498d71e24d00b1731a8ccd4f5f5126f3d9f400
|
||||
|
||||
$ ./cli.js withdraw 0x1941fa999e2b4bfeec3ce53c2440c3bc991b1b84c9bb650ea19f8331baf621001e696487e2a2ee54541fa12f49498d71e24d00b1731a8ccd4f5f5126f3d9f400 0xee6249BA80596A4890D1BD84dbf5E4322eA4E7f0
|
||||
`)
|
||||
process.exit(code)
|
||||
}
|
||||
|
||||
/** Process command line args and run */
|
||||
async function runConsole(args) {
|
||||
if (args.length === 0) {
|
||||
printHelp()
|
||||
} else {
|
||||
switch (args[0]) {
|
||||
case 'deposit':
|
||||
if (args.length === 1) {
|
||||
await init()
|
||||
await printBalance(tornado._address, 'Tornado')
|
||||
await printBalance(senderAccount, 'Sender account')
|
||||
await deposit()
|
||||
await printBalance(tornado._address, 'Tornado')
|
||||
await printBalance(senderAccount, 'Sender account')
|
||||
} else {
|
||||
printHelp(1)
|
||||
}
|
||||
break
|
||||
case 'depositErc20':
|
||||
if (args.length === 1) {
|
||||
await init()
|
||||
await printBalance(erc20tornado._address, 'Tornado')
|
||||
await printBalance(senderAccount, 'Sender account')
|
||||
await depositErc20()
|
||||
await printBalance(erc20tornado._address, 'Tornado')
|
||||
await printBalance(senderAccount, 'Sender account')
|
||||
} else {
|
||||
printHelp(1)
|
||||
}
|
||||
break
|
||||
case 'balance':
|
||||
if (args.length === 2 && /^0x[0-9a-fA-F]{40}$/.test(args[1])) {
|
||||
await init()
|
||||
await printBalance(args[1])
|
||||
} else {
|
||||
printHelp(1)
|
||||
}
|
||||
break
|
||||
case 'withdraw':
|
||||
if (args.length >= 3 && args.length <= 4 && /^0x[0-9a-fA-F]{124}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) {
|
||||
await init()
|
||||
await printBalance(tornado._address, 'Tornado')
|
||||
await printBalance(args[2], 'Recipient account')
|
||||
if (args[3]) {
|
||||
await withdrawRelay(args[1], args[2], args[3])
|
||||
} else {
|
||||
await withdraw(args[1], args[2])
|
||||
}
|
||||
await printBalance(tornado._address, 'Tornado')
|
||||
await printBalance(args[2], 'Recipient account')
|
||||
} else {
|
||||
printHelp(1)
|
||||
}
|
||||
break
|
||||
case 'withdrawErc20':
|
||||
if (args.length >= 3 && args.length <= 4 && /^0x[0-9a-fA-F]{124}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) {
|
||||
await init()
|
||||
await printBalance(erc20tornado._address, 'Tornado')
|
||||
await printBalance(args[2], 'Recipient account')
|
||||
if (args[3]) {
|
||||
await withdrawRelayErc20(args[1], args[2], args[3])
|
||||
} else {
|
||||
await withdrawErc20(args[1], args[2])
|
||||
}
|
||||
await printBalance(erc20tornado._address, 'Tornado')
|
||||
await printBalance(args[2], 'Recipient account')
|
||||
} else {
|
||||
printHelp(1)
|
||||
}
|
||||
break
|
||||
case 'test':
|
||||
if (args.length === 1) {
|
||||
await init()
|
||||
const note1 = await deposit()
|
||||
await withdraw(note1, senderAccount)
|
||||
|
||||
const note2 = await depositErc20()
|
||||
await withdrawErc20(note2, senderAccount)
|
||||
} else {
|
||||
printHelp(1)
|
||||
}
|
||||
break
|
||||
case 'testRelay':
|
||||
if (args.length === 1) {
|
||||
await init()
|
||||
const note1 = await deposit()
|
||||
await withdrawRelay(note1, senderAccount, 'http://localhost:8000')
|
||||
|
||||
const note2 = await depositErc20()
|
||||
await withdrawRelayErc20(note2, senderAccount, 'http://localhost:8000')
|
||||
} else {
|
||||
printHelp(1)
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
printHelp(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inBrowser) {
|
||||
window.deposit = deposit
|
||||
window.depositErc20 = depositErc20
|
||||
window.withdraw = async () => {
|
||||
const note = prompt('Enter the note to withdraw')
|
||||
const recipient = (await web3.eth.getAccounts())[0]
|
||||
await withdraw(note, recipient)
|
||||
}
|
||||
init()
|
||||
} else {
|
||||
runConsole(process.argv.slice(2))
|
||||
.then(() => process.exit(0))
|
||||
.catch(err => { console.log(err); process.exit(1) })
|
||||
}
|
|
@ -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.8;
|
||||
// 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.8;
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.8;
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
pragma solidity >=0.4.21 <0.6.0;
|
||||
|
||||
contract Migrations {
|
||||
address public owner;
|
||||
uint public last_completed_migration;
|
||||
|
||||
constructor() public {
|
||||
owner = msg.sender;
|
||||
}
|
||||
|
||||
modifier restricted() {
|
||||
if (msg.sender == owner) _;
|
||||
}
|
||||
|
||||
function setCompleted(uint completed) public restricted {
|
||||
last_completed_migration = completed;
|
||||
}
|
||||
|
||||
function upgrade(address new_address) public restricted {
|
||||
Migrations upgraded = Migrations(new_address);
|
||||
upgraded.setCompleted(last_completed_migration);
|
||||
}
|
||||
}
|
|
@ -1,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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -1,20 +1,33 @@
|
|||
pragma solidity ^0.5.8;
|
||||
// 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);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
pragma solidity ^0.5.8;
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
import '../MerkleTreeWithHistory.sol';
|
||||
import "../MerkleTreeWithHistory.sol";
|
||||
|
||||
contract MerkleTreeWithHistoryMock is MerkleTreeWithHistory {
|
||||
|
||||
constructor (uint32 _treeLevels) MerkleTreeWithHistory(_treeLevels) public {}
|
||||
constructor(uint32 _treeLevels, IHasher _hasher) MerkleTreeWithHistory(_treeLevels, _hasher) {}
|
||||
|
||||
function insert(bytes32 _leaf) public {
|
||||
_insert(_leaf);
|
||||
_insert(_leaf);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.8;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../build/circuits/Verifier.sol
|
|
@ -0,0 +1,231 @@
|
|||
/**
|
||||
*Submitted for verification at Etherscan.io on 2020-05-12
|
||||
*/
|
||||
|
||||
// https://tornado.cash Verifier.sol generated by trusted setup ceremony.
|
||||
/*
|
||||
* d888888P dP a88888b. dP
|
||||
* 88 88 d8' `88 88
|
||||
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
|
||||
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
|
||||
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
|
||||
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
|
||||
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
|
||||
*/
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright 2017 Christian Reitwiessner
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// 2019 OKIMS
|
||||
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
library Pairing {
|
||||
uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
|
||||
|
||||
struct G1Point {
|
||||
uint256 X;
|
||||
uint256 Y;
|
||||
}
|
||||
|
||||
// Encoding of field elements is: X[0] * z + X[1]
|
||||
struct G2Point {
|
||||
uint256[2] X;
|
||||
uint256[2] Y;
|
||||
}
|
||||
|
||||
/*
|
||||
* @return The negation of p, i.e. p.plus(p.negate()) should be zero.
|
||||
*/
|
||||
function negate(G1Point memory p) internal pure returns (G1Point memory) {
|
||||
// The prime q in the base field F_q for G1
|
||||
if (p.X == 0 && p.Y == 0) {
|
||||
return G1Point(0, 0);
|
||||
} else {
|
||||
return G1Point(p.X, PRIME_Q - (p.Y % PRIME_Q));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @return r the sum of two points of G1
|
||||
*/
|
||||
function plus(
|
||||
G1Point memory p1,
|
||||
G1Point memory p2
|
||||
) internal view returns (G1Point memory r) {
|
||||
uint256[4] memory input;
|
||||
input[0] = p1.X;
|
||||
input[1] = p1.Y;
|
||||
input[2] = p2.X;
|
||||
input[3] = p2.Y;
|
||||
bool success;
|
||||
|
||||
// solium-disable-next-line security/no-inline-assembly
|
||||
assembly {
|
||||
success := staticcall(sub(gas(), 2000), 6, input, 0xc0, r, 0x60)
|
||||
// Use "invalid" to make gas estimation work
|
||||
switch success case 0 { invalid() }
|
||||
}
|
||||
|
||||
require(success, "pairing-add-failed");
|
||||
}
|
||||
|
||||
/*
|
||||
* @return r the product of a point on G1 and a scalar, i.e.
|
||||
* p == p.scalar_mul(1) and p.plus(p) == p.scalar_mul(2) for all
|
||||
* points p.
|
||||
*/
|
||||
function scalar_mul(G1Point memory p, uint256 s) internal view returns (G1Point memory r) {
|
||||
uint256[3] memory input;
|
||||
input[0] = p.X;
|
||||
input[1] = p.Y;
|
||||
input[2] = s;
|
||||
bool success;
|
||||
// solium-disable-next-line security/no-inline-assembly
|
||||
assembly {
|
||||
success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60)
|
||||
// Use "invalid" to make gas estimation work
|
||||
switch success case 0 { invalid() }
|
||||
}
|
||||
require(success, "pairing-mul-failed");
|
||||
}
|
||||
|
||||
/* @return The result of computing the pairing check
|
||||
* e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1
|
||||
* For example,
|
||||
* pairing([P1(), P1().negate()], [P2(), P2()]) should return true.
|
||||
*/
|
||||
function pairing(
|
||||
G1Point memory a1,
|
||||
G2Point memory a2,
|
||||
G1Point memory b1,
|
||||
G2Point memory b2,
|
||||
G1Point memory c1,
|
||||
G2Point memory c2,
|
||||
G1Point memory d1,
|
||||
G2Point memory d2
|
||||
) internal view returns (bool) {
|
||||
G1Point[4] memory p1 = [a1, b1, c1, d1];
|
||||
G2Point[4] memory p2 = [a2, b2, c2, d2];
|
||||
|
||||
uint256 inputSize = 24;
|
||||
uint256[] memory input = new uint256[](inputSize);
|
||||
|
||||
for (uint256 i = 0; i < 4; i++) {
|
||||
uint256 j = i * 6;
|
||||
input[j + 0] = p1[i].X;
|
||||
input[j + 1] = p1[i].Y;
|
||||
input[j + 2] = p2[i].X[0];
|
||||
input[j + 3] = p2[i].X[1];
|
||||
input[j + 4] = p2[i].Y[0];
|
||||
input[j + 5] = p2[i].Y[1];
|
||||
}
|
||||
|
||||
uint256[1] memory out;
|
||||
bool success;
|
||||
|
||||
// solium-disable-next-line security/no-inline-assembly
|
||||
assembly {
|
||||
success := staticcall(sub(gas(), 2000), 8, add(input, 0x20), mul(inputSize, 0x20), out, 0x20)
|
||||
// Use "invalid" to make gas estimation work
|
||||
switch success case 0 { invalid() }
|
||||
}
|
||||
|
||||
require(success, "pairing-opcode-failed");
|
||||
|
||||
return out[0] != 0;
|
||||
}
|
||||
}
|
||||
|
||||
contract Verifier {
|
||||
uint256 constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
|
||||
uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
|
||||
using Pairing for *;
|
||||
|
||||
struct VerifyingKey {
|
||||
Pairing.G1Point alfa1;
|
||||
Pairing.G2Point beta2;
|
||||
Pairing.G2Point gamma2;
|
||||
Pairing.G2Point delta2;
|
||||
Pairing.G1Point[7] IC;
|
||||
}
|
||||
|
||||
struct Proof {
|
||||
Pairing.G1Point A;
|
||||
Pairing.G2Point B;
|
||||
Pairing.G1Point C;
|
||||
}
|
||||
|
||||
function verifyingKey() internal pure returns (VerifyingKey memory vk) {
|
||||
vk.alfa1 = Pairing.G1Point(uint256(20692898189092739278193869274495556617788530808486270118371701516666252877969), uint256(11713062878292653967971378194351968039596396853904572879488166084231740557279));
|
||||
vk.beta2 = Pairing.G2Point([uint256(12168528810181263706895252315640534818222943348193302139358377162645029937006), uint256(281120578337195720357474965979947690431622127986816839208576358024608803542)], [uint256(16129176515713072042442734839012966563817890688785805090011011570989315559913), uint256(9011703453772030375124466642203641636825223906145908770308724549646909480510)]);
|
||||
vk.gamma2 = Pairing.G2Point([uint256(11559732032986387107991004021392285783925812861821192530917403151452391805634), uint256(10857046999023057135944570762232829481370756359578518086990519993285655852781)], [uint256(4082367875863433681332203403145435568316851327593401208105741076214120093531), uint256(8495653923123431417604973247489272438418190587263600148770280649306958101930)]);
|
||||
vk.delta2 = Pairing.G2Point([uint256(21280594949518992153305586783242820682644996932183186320680800072133486887432), uint256(150879136433974552800030963899771162647715069685890547489132178314736470662)], [uint256(1081836006956609894549771334721413187913047383331561601606260283167615953295), uint256(11434086686358152335540554643130007307617078324975981257823476472104616196090)]);
|
||||
vk.IC[0] = Pairing.G1Point(uint256(16225148364316337376768119297456868908427925829817748684139175309620217098814), uint256(5167268689450204162046084442581051565997733233062478317813755636162413164690));
|
||||
vk.IC[1] = Pairing.G1Point(uint256(12882377842072682264979317445365303375159828272423495088911985689463022094260), uint256(19488215856665173565526758360510125932214252767275816329232454875804474844786));
|
||||
vk.IC[2] = Pairing.G1Point(uint256(13083492661683431044045992285476184182144099829507350352128615182516530014777), uint256(602051281796153692392523702676782023472744522032670801091617246498551238913));
|
||||
vk.IC[3] = Pairing.G1Point(uint256(9732465972180335629969421513785602934706096902316483580882842789662669212890), uint256(2776526698606888434074200384264824461688198384989521091253289776235602495678));
|
||||
vk.IC[4] = Pairing.G1Point(uint256(8586364274534577154894611080234048648883781955345622578531233113180532234842), uint256(21276134929883121123323359450658320820075698490666870487450985603988214349407));
|
||||
vk.IC[5] = Pairing.G1Point(uint256(4910628533171597675018724709631788948355422829499855033965018665300386637884), uint256(20532468890024084510431799098097081600480376127870299142189696620752500664302));
|
||||
vk.IC[6] = Pairing.G1Point(uint256(15335858102289947642505450692012116222827233918185150176888641903531542034017), uint256(5311597067667671581646709998171703828965875677637292315055030353779531404812));
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* @returns Whether the proof is valid given the hardcoded verifying key
|
||||
* above and the public inputs
|
||||
*/
|
||||
function verifyProof(
|
||||
bytes memory proof,
|
||||
uint256[6] memory input
|
||||
) public view returns (bool) {
|
||||
uint256[8] memory p = abi.decode(proof, (uint256[8]));
|
||||
|
||||
// Make sure that each element in the proof is less than the prime q
|
||||
for (uint8 i = 0; i < p.length; i++) {
|
||||
require(p[i] < PRIME_Q, "verifier-proof-element-gte-prime-q");
|
||||
}
|
||||
|
||||
Proof memory _proof;
|
||||
_proof.A = Pairing.G1Point(p[0], p[1]);
|
||||
_proof.B = Pairing.G2Point([p[2], p[3]], [p[4], p[5]]);
|
||||
_proof.C = Pairing.G1Point(p[6], p[7]);
|
||||
|
||||
VerifyingKey memory vk = verifyingKey();
|
||||
|
||||
// Compute the linear combination vk_x
|
||||
Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0);
|
||||
vk_x = Pairing.plus(vk_x, vk.IC[0]);
|
||||
|
||||
// Make sure that every input is less than the snark scalar field
|
||||
for (uint256 i = 0; i < input.length; i++) {
|
||||
require(input[i] < SNARK_SCALAR_FIELD, "verifier-gte-snark-scalar-field");
|
||||
vk_x = Pairing.plus(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i]));
|
||||
}
|
||||
|
||||
return Pairing.pairing(
|
||||
Pairing.negate(_proof.A),
|
||||
_proof.B,
|
||||
vk.alfa1,
|
||||
vk.beta2,
|
||||
vk_x,
|
||||
vk.gamma2,
|
||||
_proof.C,
|
||||
vk.delta2
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// https://tornado.cash
|
||||
/*
|
||||
* d888888P dP a88888b. dP
|
||||
* 88 88 d8' `88 88
|
||||
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
|
||||
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
|
||||
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
|
||||
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
|
||||
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
|
||||
*/
|
||||
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
import "./ERC20Tornado.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
|
||||
contract cTornado is ERC20Tornado {
|
||||
address public immutable governance = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
|
||||
IERC20 public immutable comp;
|
||||
|
||||
constructor(
|
||||
IERC20 _comp,
|
||||
IVerifier _verifier,
|
||||
IHasher _hasher,
|
||||
uint256 _denomination,
|
||||
uint32 _merkleTreeHeight,
|
||||
IERC20 _token
|
||||
) ERC20Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight, _token) {
|
||||
require(address(_comp) != address(0), "Invalid COMP token address");
|
||||
comp = _comp;
|
||||
}
|
||||
|
||||
/// @dev Moves earned yield of the COMP token to the tornado governance contract
|
||||
/// To make it work you might need to call `comptroller.claimComp(cPoolAddress)` first
|
||||
function claimComp() external {
|
||||
comp.transfer(governance, comp.balanceOf(address(this)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
const eth = true
|
||||
const poolSize = '1000000000000000000'
|
||||
const hasherAddress = '0x83584f83f26aF4eDDA9CBe8C730bc87C364b28fe'
|
||||
const verifierAddress = '0xce172ce1F20EC0B3728c9965470eaf994A03557A'
|
||||
const deployerAddress = '0xCEe71753C9820f063b38FDbE4cFDAf1d3D928A80'
|
||||
const deploySalt = '0x0000000000000000000000000000000000000000000000000000000047941987'
|
||||
const rpcUrl = 'https://mainnet.infura.io'
|
||||
|
||||
const Web3 = require('web3')
|
||||
const web3 = new Web3(rpcUrl)
|
||||
|
||||
const contractData = require('./build/contracts/' + (eth ? 'ETHTornado.json' : 'ERC20Tornado.json'))
|
||||
const contract = new web3.eth.Contract(contractData.abi)
|
||||
const bytes = contract
|
||||
.deploy({
|
||||
data: contractData.bytecode,
|
||||
arguments: [verifierAddress, hasherAddress, poolSize, 20],
|
||||
})
|
||||
.encodeABI()
|
||||
|
||||
console.log('Deploy bytecode', bytes)
|
||||
|
||||
const deployer = new web3.eth.Contract(require('./build/contracts/IDeployer.json').abi, deployerAddress)
|
||||
const receipt = deployer.methods.deploy(bytes, deploySalt)
|
||||
receipt.then(console.log).catch(console.log)
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Binary file not shown.
After Width: | Height: | Size: 205 KiB |
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
29
index.html
29
index.html
|
@ -1,16 +1,17 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>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>
|
||||
|
|
|
@ -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
|
13
lib/MiMC.js
13
lib/MiMC.js
|
@ -1,13 +0,0 @@
|
|||
const circomlib = require('circomlib')
|
||||
const mimcsponge = circomlib.mimcsponge
|
||||
const snarkjs = require('snarkjs')
|
||||
|
||||
const bigInt = snarkjs.bigInt
|
||||
|
||||
class MimcSpongeHasher {
|
||||
hash(level, left, right) {
|
||||
return mimcsponge.multiHash([bigInt(left), bigInt(right)]).toString()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MimcSpongeHasher
|
|
@ -1,39 +0,0 @@
|
|||
|
||||
|
||||
class JsStorage {
|
||||
constructor() {
|
||||
this.db = {}
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.db[key]
|
||||
}
|
||||
|
||||
get_or_element(key, defaultElement) {
|
||||
const element = this.db[key]
|
||||
if (element === undefined) {
|
||||
return defaultElement
|
||||
} else {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
put(key, value) {
|
||||
if (key === undefined || value === undefined) {
|
||||
throw Error('key or value is undefined')
|
||||
}
|
||||
this.db[key] = value
|
||||
}
|
||||
|
||||
del(key) {
|
||||
delete this.db[key]
|
||||
}
|
||||
|
||||
put_batch(key_values) {
|
||||
key_values.forEach(element => {
|
||||
this.db[element.key] = element.value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JsStorage
|
|
@ -1,9 +0,0 @@
|
|||
/* global artifacts */
|
||||
const Migrations = artifacts.require('Migrations')
|
||||
|
||||
module.exports = function(deployer) {
|
||||
if(deployer.network === 'mainnet') {
|
||||
return
|
||||
}
|
||||
deployer.deploy(Migrations)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* global artifacts */
|
||||
const Verifier = artifacts.require('Verifier')
|
||||
|
||||
module.exports = function(deployer) {
|
||||
module.exports = function (deployer) {
|
||||
deployer.deploy(Verifier)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
|
@ -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,32 +19,46 @@
|
|||
"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 .",
|
||||
"flat": "npx truffle-flattener contracts/ETHTornado.sol > ETHTornado_flat.sol && npx truffle-flattener contracts/ERC20Tornado.sol > ERC20Tornado_flat.sol"
|
||||
"eslint": "eslint --ext .js --ignore-path .gitignore .",
|
||||
"prettier:check": "prettier --check . --config .prettierrc",
|
||||
"prettier:fix": "prettier --write . --config .prettierrc",
|
||||
"lint": "yarn eslint && yarn prettier:check",
|
||||
"flat": "npx truffle-flattener contracts/ETHTornado.sol > ETHTornado_flat.sol && npx truffle-flattener contracts/ERC20Tornado.sol > ERC20Tornado_flat.sol",
|
||||
"download": "node scripts/downloadKeys.js",
|
||||
"coverage": "yarn truffle run coverage"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "^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",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"circom": "0.0.34",
|
||||
"circomlib": "git+https://github.com/tornadocash/circomlib.git#347822604996bf25f659f96ee0f02810a1f71bb0",
|
||||
"circom": "^0.0.35",
|
||||
"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",
|
||||
"snarkjs": "git+https://github.com/peppersec/snarkjs.git#869181cfaf7526fe8972073d31655493a04326d5",
|
||||
"truffle": "^5.0.44",
|
||||
"prettier": "^2.2.1",
|
||||
"prettier-plugin-solidity": "^1.0.0-beta.3",
|
||||
"snarkjs": "git+https://github.com/tornadocash/snarkjs.git#869181cfaf7526fe8972073d31655493a04326d5",
|
||||
"solhint-plugin-prettier": "^0.0.5",
|
||||
"truffle": "^5.1.67",
|
||||
"truffle-flattener": "^1.4.2",
|
||||
"web3": "^1.2.2",
|
||||
"web3-utils": "^1.2.2",
|
||||
"websnark": "git+https://github.com/peppersec/websnark.git#c254b5962287b788081be1047fa0041c2885b39f"
|
||||
"web3": "^1.3.4",
|
||||
"web3-utils": "^1.3.4",
|
||||
"websnark": "git+https://github.com/tornadocash/websnark.git#4c0af6a8b65aabea3c09f377f63c44e7a58afa6d",
|
||||
"solidity-coverage": "^0.7.20"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
// Generates Hasher artifact at compile-time using Truffle's external compiler
|
||||
// mechanism
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const genContract = require('circomlib/src/mimcsponge_gencontract.js')
|
||||
|
||||
// where Truffle will expect to find the results of the external compiler
|
||||
// command
|
||||
const outputPath = path.join(__dirname, '..', 'build', 'Hasher.json')
|
||||
|
||||
function main() {
|
||||
const contract = {
|
||||
contractName: 'Hasher',
|
||||
abi: genContract.abi,
|
||||
bytecode: genContract.createCode('mimcsponge', 220),
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(contract))
|
||||
}
|
||||
|
||||
main()
|
|
@ -0,0 +1,43 @@
|
|||
const axios = require('axios')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const files = ['withdraw.json', 'withdraw_proving_key.bin', 'Verifier.sol', 'withdraw_verification_key.json']
|
||||
const circuitsPath = __dirname + '/../build/circuits'
|
||||
const contractsPath = __dirname + '/../build/contracts'
|
||||
|
||||
async function downloadFile({ url, path }) {
|
||||
const writer = fs.createWriteStream(path)
|
||||
|
||||
const response = await axios({
|
||||
url,
|
||||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
})
|
||||
|
||||
response.data.pipe(writer)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', resolve)
|
||||
writer.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const release = await axios.get('https://api.github.com/repos/tornadocash/tornado-core/releases/latest')
|
||||
const { assets } = release.data
|
||||
if (!fs.existsSync(circuitsPath)) {
|
||||
fs.mkdirSync(circuitsPath, { recursive: true })
|
||||
fs.mkdirSync(contractsPath, { recursive: true })
|
||||
}
|
||||
for (let asset of assets) {
|
||||
if (files.includes(asset.name)) {
|
||||
console.log(`Downloading ${asset.name} ...`)
|
||||
await downloadFile({
|
||||
url: asset.browser_download_url,
|
||||
path: path.resolve(__dirname, circuitsPath, asset.name),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
|
@ -6,7 +6,7 @@ function send(method, params = []) {
|
|||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method,
|
||||
params
|
||||
params,
|
||||
}, (err, res) => {
|
||||
return err ? reject(err) : resolve(res)
|
||||
})
|
||||
|
@ -48,5 +48,5 @@ module.exports = {
|
|||
minerStop,
|
||||
minerStart,
|
||||
increaseTime,
|
||||
traceTransaction
|
||||
traceTransaction,
|
||||
}
|
|
@ -0,0 +1,640 @@
|
|||
#!/usr/bin/env node
|
||||
// Temporary demo client
|
||||
// Works both in browser and node.js
|
||||
|
||||
require('dotenv').config()
|
||||
const fs = require('fs')
|
||||
const axios = require('axios')
|
||||
const assert = require('assert')
|
||||
const snarkjs = require('snarkjs')
|
||||
const crypto = require('crypto')
|
||||
const circomlib = require('circomlib')
|
||||
const bigInt = snarkjs.bigInt
|
||||
const merkleTree = require('fixed-merkle-tree')
|
||||
const Web3 = require('web3')
|
||||
const buildGroth16 = require('websnark/src/groth16')
|
||||
const websnarkUtils = require('websnark/src/utils')
|
||||
const { toWei, fromWei, toBN, BN } = require('web3-utils')
|
||||
const config = require('./config')
|
||||
const program = require('commander')
|
||||
|
||||
let web3, tornado, circuit, proving_key, groth16, erc20, senderAccount, netId
|
||||
let MERKLE_TREE_HEIGHT, ETH_AMOUNT, TOKEN_AMOUNT, PRIVATE_KEY
|
||||
|
||||
/** Whether we are in a browser or node.js */
|
||||
const inBrowser = (typeof window !== 'undefined')
|
||||
let isLocalRPC = false
|
||||
|
||||
/** Generate random number of specified byte length */
|
||||
const rbigint = nbytes => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
|
||||
|
||||
/** Compute pedersen hash */
|
||||
const pedersenHash = data => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
|
||||
|
||||
/** BigNumber to hex string of specified length */
|
||||
function toHex(number, length = 32) {
|
||||
const str = number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)
|
||||
return '0x' + str.padStart(length * 2, '0')
|
||||
}
|
||||
|
||||
/** Display ETH account balance */
|
||||
async function printETHBalance({ address, name }) {
|
||||
console.log(`${name} ETH balance is`, web3.utils.fromWei(await web3.eth.getBalance(address)))
|
||||
}
|
||||
|
||||
/** Display ERC20 account balance */
|
||||
async function printERC20Balance({ address, name, tokenAddress }) {
|
||||
const erc20ContractJson = require(__dirname + '/../build/contracts/ERC20Mock.json')
|
||||
erc20 = tokenAddress ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : erc20
|
||||
console.log(`${name} Token Balance is`, web3.utils.fromWei(await erc20.methods.balanceOf(address).call()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create deposit object from secret and nullifier
|
||||
*/
|
||||
function createDeposit({ nullifier, secret }) {
|
||||
const deposit = { nullifier, secret }
|
||||
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
|
||||
deposit.commitment = pedersenHash(deposit.preimage)
|
||||
deposit.commitmentHex = toHex(deposit.commitment)
|
||||
deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
|
||||
deposit.nullifierHex = toHex(deposit.nullifierHash)
|
||||
return deposit
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a deposit
|
||||
* @param currency Сurrency
|
||||
* @param amount Deposit amount
|
||||
*/
|
||||
async function deposit({ currency, amount }) {
|
||||
const deposit = createDeposit({ nullifier: rbigint(31), secret: rbigint(31) })
|
||||
const note = toHex(deposit.preimage, 62)
|
||||
const noteString = `tornado-${currency}-${amount}-${netId}-${note}`
|
||||
console.log(`Your note: ${noteString}`)
|
||||
if (currency === 'eth') {
|
||||
await printETHBalance({ address: tornado._address, name: 'Tornado' })
|
||||
await printETHBalance({ address: senderAccount, name: 'Sender account' })
|
||||
const value = isLocalRPC ? ETH_AMOUNT : fromDecimals({ amount, decimals: 18 })
|
||||
console.log('Submitting deposit transaction')
|
||||
await tornado.methods.deposit(toHex(deposit.commitment)).send({ value, from: senderAccount, gas: 2e6 })
|
||||
await printETHBalance({ address: tornado._address, name: 'Tornado' })
|
||||
await printETHBalance({ address: senderAccount, name: 'Sender account' })
|
||||
} else { // a token
|
||||
await printERC20Balance({ address: tornado._address, name: 'Tornado' })
|
||||
await printERC20Balance({ address: senderAccount, name: 'Sender account' })
|
||||
const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
|
||||
const tokenAmount = isLocalRPC ? TOKEN_AMOUNT : fromDecimals({ amount, decimals })
|
||||
if (isLocalRPC) {
|
||||
console.log('Minting some test tokens to deposit')
|
||||
await erc20.methods.mint(senderAccount, tokenAmount).send({ from: senderAccount, gas: 2e6 })
|
||||
}
|
||||
|
||||
const allowance = await erc20.methods.allowance(senderAccount, tornado._address).call({ from: senderAccount })
|
||||
console.log('Current allowance is', fromWei(allowance))
|
||||
if (toBN(allowance).lt(toBN(tokenAmount))) {
|
||||
console.log('Approving tokens for deposit')
|
||||
await erc20.methods.approve(tornado._address, tokenAmount).send({ from: senderAccount, gas: 1e6 })
|
||||
}
|
||||
|
||||
console.log('Submitting deposit transaction')
|
||||
await tornado.methods.deposit(toHex(deposit.commitment)).send({ from: senderAccount, gas: 2e6 })
|
||||
await printERC20Balance({ address: tornado._address, name: 'Tornado' })
|
||||
await printERC20Balance({ address: senderAccount, name: 'Sender account' })
|
||||
}
|
||||
|
||||
return noteString
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate merkle tree for a deposit.
|
||||
* Download deposit events from the tornado, reconstructs merkle tree, finds our deposit leaf
|
||||
* in it and generates merkle proof
|
||||
* @param deposit Deposit object
|
||||
*/
|
||||
async function generateMerkleProof(deposit) {
|
||||
// Get all deposit events from smart contract and assemble merkle tree from them
|
||||
console.log('Getting current state from tornado contract')
|
||||
const events = await tornado.getPastEvents('Deposit', { fromBlock: 0, toBlock: 'latest' })
|
||||
const leaves = events
|
||||
.sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order
|
||||
.map(e => e.returnValues.commitment)
|
||||
const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves)
|
||||
|
||||
// Find current commitment in the tree
|
||||
const depositEvent = events.find(e => e.returnValues.commitment === toHex(deposit.commitment))
|
||||
const leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
|
||||
|
||||
// Validate that our data is correct
|
||||
const root = tree.root()
|
||||
const isValidRoot = await tornado.methods.isKnownRoot(toHex(root)).call()
|
||||
const isSpent = await tornado.methods.isSpent(toHex(deposit.nullifierHash)).call()
|
||||
assert(isValidRoot === true, 'Merkle tree is corrupted')
|
||||
assert(isSpent === false, 'The note is already spent')
|
||||
assert(leafIndex >= 0, 'The deposit is not found in the tree')
|
||||
|
||||
// Compute merkle proof of our commitment
|
||||
const { pathElements, pathIndices } = tree.path(leafIndex)
|
||||
return { pathElements, pathIndices, root: tree.root() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SNARK proof for withdrawal
|
||||
* @param deposit Deposit object
|
||||
* @param recipient Funds recipient
|
||||
* @param relayer Relayer address
|
||||
* @param fee Relayer fee
|
||||
* @param refund Receive ether for exchanged tokens
|
||||
*/
|
||||
async function generateProof({ deposit, recipient, relayerAddress = 0, fee = 0, refund = 0 }) {
|
||||
// Compute merkle proof of our commitment
|
||||
const { root, pathElements, pathIndices } = await generateMerkleProof(deposit)
|
||||
|
||||
// Prepare circuit input
|
||||
const input = {
|
||||
// Public snark inputs
|
||||
root: root,
|
||||
nullifierHash: deposit.nullifierHash,
|
||||
recipient: bigInt(recipient),
|
||||
relayer: bigInt(relayerAddress),
|
||||
fee: bigInt(fee),
|
||||
refund: bigInt(refund),
|
||||
|
||||
// Private snark inputs
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
}
|
||||
|
||||
console.log('Generating SNARK proof')
|
||||
console.time('Proof time')
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
console.timeEnd('Proof time')
|
||||
|
||||
const args = [
|
||||
toHex(input.root),
|
||||
toHex(input.nullifierHash),
|
||||
toHex(input.recipient, 20),
|
||||
toHex(input.relayer, 20),
|
||||
toHex(input.fee),
|
||||
toHex(input.refund),
|
||||
]
|
||||
|
||||
return { proof, args }
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an ETH withdrawal
|
||||
* @param noteString Note to withdraw
|
||||
* @param recipient Recipient address
|
||||
*/
|
||||
async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund = '0' }) {
|
||||
if (currency === 'eth' && refund !== '0') {
|
||||
throw new Error('The ETH purchase is supposted to be 0 for ETH withdrawals')
|
||||
}
|
||||
refund = toWei(refund)
|
||||
if (relayerURL) {
|
||||
if (relayerURL.endsWith('.eth')) {
|
||||
throw new Error('ENS name resolving is not supported. Please provide DNS name of the relayer. See instuctions in README.md')
|
||||
}
|
||||
const relayerStatus = await axios.get(relayerURL + '/status')
|
||||
const { relayerAddress, netId, gasPrices, ethPrices, relayerServiceFee } = relayerStatus.data
|
||||
assert(netId === await web3.eth.net.getId() || netId === '*', 'This relay is for different network')
|
||||
console.log('Relay address: ', relayerAddress)
|
||||
|
||||
const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
|
||||
const fee = calculateFee({ gasPrices, currency, amount, refund, ethPrices, relayerServiceFee, decimals })
|
||||
if (fee.gt(fromDecimals({ amount, decimals }))) {
|
||||
throw new Error('Too high refund')
|
||||
}
|
||||
const { proof, args } = await generateProof({ deposit, recipient, relayerAddress, fee, refund })
|
||||
|
||||
console.log('Sending withdraw transaction through relay')
|
||||
try {
|
||||
const relay = await axios.post(relayerURL + '/relay', { contract: tornado._address, proof, args })
|
||||
if (netId === 1 || netId === 42) {
|
||||
console.log(`Transaction submitted through the relay. View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${relay.data.txHash}`)
|
||||
} else {
|
||||
console.log(`Transaction submitted through the relay. The transaction hash is ${relay.data.txHash}`)
|
||||
}
|
||||
|
||||
const receipt = await waitForTxReceipt({ txHash: relay.data.txHash })
|
||||
console.log('Transaction mined in block', receipt.blockNumber)
|
||||
} catch (e) {
|
||||
if (e.response) {
|
||||
console.error(e.response.data.error)
|
||||
} else {
|
||||
console.error(e.message)
|
||||
}
|
||||
}
|
||||
} else { // using private key
|
||||
const { proof, args } = await generateProof({ deposit, recipient, refund })
|
||||
|
||||
console.log('Submitting withdraw transaction')
|
||||
await tornado.methods.withdraw(proof, ...args).send({ from: senderAccount, value: refund.toString(), gas: 1e6 })
|
||||
.on('transactionHash', function (txHash) {
|
||||
if (netId === 1 || netId === 42) {
|
||||
console.log(`View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${txHash}`)
|
||||
} else {
|
||||
console.log(`The transaction hash is ${txHash}`)
|
||||
}
|
||||
}).on('error', function (e) {
|
||||
console.error('on transactionHash error', e.message)
|
||||
})
|
||||
}
|
||||
console.log('Done')
|
||||
}
|
||||
|
||||
function fromDecimals({ amount, decimals }) {
|
||||
amount = amount.toString()
|
||||
let ether = amount.toString()
|
||||
const base = new BN('10').pow(new BN(decimals))
|
||||
const baseLength = base.toString(10).length - 1 || 1
|
||||
|
||||
const negative = ether.substring(0, 1) === '-'
|
||||
if (negative) {
|
||||
ether = ether.substring(1)
|
||||
}
|
||||
|
||||
if (ether === '.') {
|
||||
throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, invalid value')
|
||||
}
|
||||
|
||||
// Split it into a whole and fractional part
|
||||
const comps = ether.split('.')
|
||||
if (comps.length > 2) {
|
||||
throw new Error(
|
||||
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal points',
|
||||
)
|
||||
}
|
||||
|
||||
let whole = comps[0]
|
||||
let fraction = comps[1]
|
||||
|
||||
if (!whole) {
|
||||
whole = '0'
|
||||
}
|
||||
if (!fraction) {
|
||||
fraction = '0'
|
||||
}
|
||||
if (fraction.length > baseLength) {
|
||||
throw new Error(
|
||||
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal places',
|
||||
)
|
||||
}
|
||||
|
||||
while (fraction.length < baseLength) {
|
||||
fraction += '0'
|
||||
}
|
||||
|
||||
whole = new BN(whole)
|
||||
fraction = new BN(fraction)
|
||||
let wei = whole.mul(base).add(fraction)
|
||||
|
||||
if (negative) {
|
||||
wei = wei.mul(negative)
|
||||
}
|
||||
|
||||
return new BN(wei.toString(10), 10)
|
||||
}
|
||||
|
||||
function toDecimals(value, decimals, fixed) {
|
||||
const zero = new BN(0)
|
||||
const negative1 = new BN(-1)
|
||||
decimals = decimals || 18
|
||||
fixed = fixed || 7
|
||||
|
||||
value = new BN(value)
|
||||
const negative = value.lt(zero)
|
||||
const base = new BN('10').pow(new BN(decimals))
|
||||
const baseLength = base.toString(10).length - 1 || 1
|
||||
|
||||
if (negative) {
|
||||
value = value.mul(negative1)
|
||||
}
|
||||
|
||||
let fraction = value.mod(base).toString(10)
|
||||
while (fraction.length < baseLength) {
|
||||
fraction = `0${fraction}`
|
||||
}
|
||||
fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1]
|
||||
|
||||
const whole = value.div(base).toString(10)
|
||||
value = `${whole}${fraction === '0' ? '' : `.${fraction}`}`
|
||||
|
||||
if (negative) {
|
||||
value = `-${value}`
|
||||
}
|
||||
|
||||
if (fixed) {
|
||||
value = value.slice(0, fixed)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function getCurrentNetworkName() {
|
||||
switch (netId) {
|
||||
case 1:
|
||||
return ''
|
||||
case 42:
|
||||
return 'kovan.'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function calculateFee({ gasPrices, currency, amount, refund, ethPrices, relayerServiceFee, decimals }) {
|
||||
const decimalsPoint = Math.floor(relayerServiceFee) === Number(relayerServiceFee) ?
|
||||
0 :
|
||||
relayerServiceFee.toString().split('.')[1].length
|
||||
const roundDecimal = 10 ** decimalsPoint
|
||||
const total = toBN(fromDecimals({ amount, decimals }))
|
||||
const feePercent = total.mul(toBN(relayerServiceFee * roundDecimal)).div(toBN(roundDecimal * 100))
|
||||
const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(5e5))
|
||||
let desiredFee
|
||||
switch (currency) {
|
||||
case 'eth': {
|
||||
desiredFee = expense.add(feePercent)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
desiredFee = expense.add(toBN(refund))
|
||||
.mul(toBN(10 ** decimals))
|
||||
.div(toBN(ethPrices[currency]))
|
||||
desiredFee = desiredFee.add(feePercent)
|
||||
break
|
||||
}
|
||||
}
|
||||
return desiredFee
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for transaction to be mined
|
||||
* @param txHash Hash of transaction
|
||||
* @param attempts
|
||||
* @param delay
|
||||
*/
|
||||
function waitForTxReceipt({ txHash, attempts = 60, delay = 1000 }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkForTx = async (txHash, retryAttempt = 0) => {
|
||||
const result = await web3.eth.getTransactionReceipt(txHash)
|
||||
if (!result || !result.blockNumber) {
|
||||
if (retryAttempt <= attempts) {
|
||||
setTimeout(() => checkForTx(txHash, retryAttempt + 1), delay)
|
||||
} else {
|
||||
reject(new Error('tx was not mined'))
|
||||
}
|
||||
} else {
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
checkForTx(txHash)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Tornado.cash note
|
||||
* @param noteString the note
|
||||
*/
|
||||
function parseNote(noteString) {
|
||||
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g
|
||||
const match = noteRegex.exec(noteString)
|
||||
if (!match) {
|
||||
throw new Error('The note has invalid format')
|
||||
}
|
||||
|
||||
const buf = Buffer.from(match.groups.note, 'hex')
|
||||
const nullifier = bigInt.leBuff2int(buf.slice(0, 31))
|
||||
const secret = bigInt.leBuff2int(buf.slice(31, 62))
|
||||
const deposit = createDeposit({ nullifier, secret })
|
||||
const netId = Number(match.groups.netId)
|
||||
|
||||
return { currency: match.groups.currency, amount: match.groups.amount, netId, deposit }
|
||||
}
|
||||
|
||||
async function loadDepositData({ deposit }) {
|
||||
try {
|
||||
const eventWhenHappened = await tornado.getPastEvents('Deposit', {
|
||||
filter: {
|
||||
commitment: deposit.commitmentHex,
|
||||
},
|
||||
fromBlock: 0,
|
||||
toBlock: 'latest',
|
||||
})
|
||||
if (eventWhenHappened.length === 0) {
|
||||
throw new Error('There is no related deposit, the note is invalid')
|
||||
}
|
||||
|
||||
const { timestamp } = eventWhenHappened[0].returnValues
|
||||
const txHash = eventWhenHappened[0].transactionHash
|
||||
const isSpent = await tornado.methods.isSpent(deposit.nullifierHex).call()
|
||||
const receipt = await web3.eth.getTransactionReceipt(txHash)
|
||||
|
||||
return { timestamp, txHash, isSpent, from: receipt.from, commitment: deposit.commitmentHex }
|
||||
} catch (e) {
|
||||
console.error('loadDepositData', e)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
async function loadWithdrawalData({ amount, currency, deposit }) {
|
||||
try {
|
||||
const events = await tornado.getPastEvents('Withdrawal', {
|
||||
fromBlock: 0,
|
||||
toBlock: 'latest',
|
||||
})
|
||||
|
||||
const withdrawEvent = events.filter((event) => {
|
||||
return event.returnValues.nullifierHash === deposit.nullifierHex
|
||||
})[0]
|
||||
|
||||
const fee = withdrawEvent.returnValues.fee
|
||||
const decimals = config.deployments[`netId${netId}`][currency].decimals
|
||||
const withdrawalAmount = toBN(fromDecimals({ amount, decimals })).sub(
|
||||
toBN(fee),
|
||||
)
|
||||
const { timestamp } = await web3.eth.getBlock(withdrawEvent.blockHash)
|
||||
return {
|
||||
amount: toDecimals(withdrawalAmount, decimals, 9),
|
||||
txHash: withdrawEvent.transactionHash,
|
||||
to: withdrawEvent.returnValues.to,
|
||||
timestamp,
|
||||
nullifier: deposit.nullifierHex,
|
||||
fee: toDecimals(fee, decimals, 9),
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('loadWithdrawalData', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init web3, contracts, and snark
|
||||
*/
|
||||
async function init({ rpc, noteNetId, currency = 'dai', amount = '100' }) {
|
||||
let contractJson, erc20ContractJson, erc20tornadoJson, tornadoAddress, tokenAddress
|
||||
// TODO do we need this? should it work in browser really?
|
||||
if (inBrowser) {
|
||||
// Initialize using injected web3 (Metamask)
|
||||
// To assemble web version run `npm run browserify`
|
||||
web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 })
|
||||
contractJson = await (await fetch('build/contracts/ETHTornado.json')).json()
|
||||
circuit = await (await fetch('build/circuits/withdraw.json')).json()
|
||||
proving_key = await (await fetch('build/circuits/withdraw_proving_key.bin')).arrayBuffer()
|
||||
MERKLE_TREE_HEIGHT = 20
|
||||
ETH_AMOUNT = 1e18
|
||||
TOKEN_AMOUNT = 1e19
|
||||
senderAccount = (await web3.eth.getAccounts())[0]
|
||||
} else {
|
||||
// Initialize from local node
|
||||
web3 = new Web3(rpc, null, { transactionConfirmationBlocks: 1 })
|
||||
contractJson = require(__dirname + '/../build/contracts/ETHTornado.json')
|
||||
circuit = require(__dirname + '/../build/circuits/withdraw.json')
|
||||
proving_key = fs.readFileSync(__dirname + '/../build/circuits/withdraw_proving_key.bin').buffer
|
||||
MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT || 20
|
||||
ETH_AMOUNT = process.env.ETH_AMOUNT
|
||||
TOKEN_AMOUNT = process.env.TOKEN_AMOUNT
|
||||
PRIVATE_KEY = process.env.PRIVATE_KEY
|
||||
if (PRIVATE_KEY) {
|
||||
const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY)
|
||||
web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY)
|
||||
web3.eth.defaultAccount = account.address
|
||||
senderAccount = account.address
|
||||
} else {
|
||||
console.log('Warning! PRIVATE_KEY not found. Please provide PRIVATE_KEY in .env file if you deposit')
|
||||
}
|
||||
erc20ContractJson = require(__dirname + '/../build/contracts/ERC20Mock.json')
|
||||
erc20tornadoJson = require(__dirname + '/../build/contracts/ERC20Tornado.json')
|
||||
}
|
||||
// groth16 initialises a lot of Promises that will never be resolved, that's why we need to use process.exit to terminate the CLI
|
||||
groth16 = await buildGroth16()
|
||||
netId = await web3.eth.net.getId()
|
||||
if (noteNetId && Number(noteNetId) !== netId) {
|
||||
throw new Error('This note is for a different network. Specify the --rpc option explicitly')
|
||||
}
|
||||
isLocalRPC = netId > 42
|
||||
|
||||
if (isLocalRPC) {
|
||||
tornadoAddress = currency === 'eth' ? contractJson.networks[netId].address : erc20tornadoJson.networks[netId].address
|
||||
tokenAddress = currency !== 'eth' ? erc20ContractJson.networks[netId].address : null
|
||||
senderAccount = (await web3.eth.getAccounts())[0]
|
||||
} else {
|
||||
try {
|
||||
tornadoAddress = config.deployments[`netId${netId}`][currency].instanceAddress[amount]
|
||||
if (!tornadoAddress) {
|
||||
throw new Error()
|
||||
}
|
||||
tokenAddress = config.deployments[`netId${netId}`][currency].tokenAddress
|
||||
} catch (e) {
|
||||
console.error('There is no such tornado instance, check the currency and amount you provide')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
tornado = new web3.eth.Contract(contractJson.abi, tornadoAddress)
|
||||
erc20 = currency !== 'eth' ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : {}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (inBrowser) {
|
||||
const instance = { currency: 'eth', amount: '0.1' }
|
||||
await init(instance)
|
||||
window.deposit = async () => {
|
||||
await deposit(instance)
|
||||
}
|
||||
window.withdraw = async () => {
|
||||
const noteString = prompt('Enter the note to withdraw')
|
||||
const recipient = (await web3.eth.getAccounts())[0]
|
||||
|
||||
const { currency, amount, netId, deposit } = parseNote(noteString)
|
||||
await init({ noteNetId: netId, currency, amount })
|
||||
await withdraw({ deposit, currency, amount, recipient })
|
||||
}
|
||||
} else {
|
||||
program
|
||||
.option('-r, --rpc <URL>', 'The RPC, CLI should interact with', 'http://localhost:8545')
|
||||
.option('-R, --relayer <URL>', 'Withdraw via relayer')
|
||||
program
|
||||
.command('deposit <currency> <amount>')
|
||||
.description('Submit a deposit of specified currency and amount from default eth account and return the resulting note. The currency is one of (ETH|DAI|cDAI|USDC|cUSDC|USDT). The amount depends on currency, see config.js file or visit https://tornado.cash.')
|
||||
.action(async (currency, amount) => {
|
||||
currency = currency.toLowerCase()
|
||||
await init({ rpc: program.rpc, currency, amount })
|
||||
await deposit({ currency, amount })
|
||||
})
|
||||
program
|
||||
.command('withdraw <note> <recipient> [ETH_purchase]')
|
||||
.description('Withdraw a note to a recipient account using relayer or specified private key. You can exchange some of your deposit`s tokens to ETH during the withdrawal by specifing ETH_purchase (e.g. 0.01) to pay for gas in future transactions. Also see the --relayer option.')
|
||||
.action(async (noteString, recipient, refund) => {
|
||||
const { currency, amount, netId, deposit } = parseNote(noteString)
|
||||
await init({ rpc: program.rpc, noteNetId: netId, currency, amount })
|
||||
await withdraw({ deposit, currency, amount, recipient, refund, relayerURL: program.relayer })
|
||||
})
|
||||
program
|
||||
.command('balance <address> [token_address]')
|
||||
.description('Check ETH and ERC20 balance')
|
||||
.action(async (address, tokenAddress) => {
|
||||
await init({ rpc: program.rpc })
|
||||
await printETHBalance({ address, name: '' })
|
||||
if (tokenAddress) {
|
||||
await printERC20Balance({ address, name: '', tokenAddress })
|
||||
}
|
||||
})
|
||||
program
|
||||
.command('compliance <note>')
|
||||
.description('Shows the deposit and withdrawal of the provided note. This might be necessary to show the origin of assets held in your withdrawal address.')
|
||||
.action(async (noteString) => {
|
||||
const { currency, amount, netId, deposit } = parseNote(noteString)
|
||||
await init({ rpc: program.rpc, noteNetId: netId, currency, amount })
|
||||
const depositInfo = await loadDepositData({ deposit })
|
||||
const depositDate = new Date(depositInfo.timestamp * 1000)
|
||||
console.log('\n=============Deposit=================')
|
||||
console.log('Deposit :', amount, currency)
|
||||
console.log('Date :', depositDate.toLocaleDateString(), depositDate.toLocaleTimeString())
|
||||
console.log('From :', `https://${getCurrentNetworkName()}etherscan.io/address/${depositInfo.from}`)
|
||||
console.log('Transaction :', `https://${getCurrentNetworkName()}etherscan.io/tx/${depositInfo.txHash}`)
|
||||
console.log('Commitment :', depositInfo.commitment)
|
||||
if (deposit.isSpent) {
|
||||
console.log('The note was not spent')
|
||||
}
|
||||
|
||||
const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit })
|
||||
const withdrawalDate = new Date(withdrawInfo.timestamp * 1000)
|
||||
console.log('\n=============Withdrawal==============')
|
||||
console.log('Withdrawal :', withdrawInfo.amount, currency)
|
||||
console.log('Relayer Fee :', withdrawInfo.fee, currency)
|
||||
console.log('Date :', withdrawalDate.toLocaleDateString(), withdrawalDate.toLocaleTimeString())
|
||||
console.log('To :', `https://${getCurrentNetworkName()}etherscan.io/address/${withdrawInfo.to}`)
|
||||
console.log('Transaction :', `https://${getCurrentNetworkName()}etherscan.io/tx/${withdrawInfo.txHash}`)
|
||||
console.log('Nullifier :', withdrawInfo.nullifier)
|
||||
})
|
||||
program
|
||||
.command('test')
|
||||
.description('Perform an automated test. It deposits and withdraws one ETH and one ERC20 note. Uses ganache.')
|
||||
.action(async () => {
|
||||
console.log('Start performing ETH deposit-withdraw test')
|
||||
let currency = 'eth'
|
||||
let amount = '0.1'
|
||||
await init({ rpc: program.rpc, currency, amount })
|
||||
let noteString = await deposit({ currency, amount })
|
||||
let parsedNote = parseNote(noteString)
|
||||
await withdraw({ deposit: parsedNote.deposit, currency, amount, recipient: senderAccount, relayerURL: program.relayer })
|
||||
|
||||
console.log('\nStart performing DAI deposit-withdraw test')
|
||||
currency = 'dai'
|
||||
amount = '100'
|
||||
await init({ rpc: program.rpc, currency, amount })
|
||||
noteString = await deposit({ currency, amount })
|
||||
; (parsedNote = parseNote(noteString))
|
||||
await withdraw({ deposit: parsedNote.deposit, currency, amount, recipient: senderAccount, refund: '0.02', relayerURL: program.relayer })
|
||||
})
|
||||
try {
|
||||
await program.parseAsync(process.argv)
|
||||
process.exit(0)
|
||||
} catch (e) {
|
||||
console.log('Error:', e)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
|
@ -0,0 +1,140 @@
|
|||
require('dotenv').config()
|
||||
|
||||
module.exports = {
|
||||
deployments: {
|
||||
netId1: {
|
||||
eth: {
|
||||
instanceAddress: {
|
||||
0.1: '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
|
||||
1: '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
|
||||
10: '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
|
||||
100: '0xA160cdAB225685dA1d56aa342Ad8841c3b53f291',
|
||||
},
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
dai: {
|
||||
instanceAddress: {
|
||||
100: '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
|
||||
1000: '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
|
||||
10000: '0xF60dD140cFf0706bAE9Cd734Ac3ae76AD9eBC32A',
|
||||
100000: undefined,
|
||||
},
|
||||
tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
|
||||
symbol: 'DAI',
|
||||
decimals: 18,
|
||||
},
|
||||
cdai: {
|
||||
instanceAddress: {
|
||||
5000: '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
|
||||
50000: '0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659',
|
||||
500000: '0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00',
|
||||
5000000: undefined,
|
||||
},
|
||||
tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643',
|
||||
symbol: 'cDAI',
|
||||
decimals: 8,
|
||||
},
|
||||
usdc: {
|
||||
instanceAddress: {
|
||||
100: '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
|
||||
1000: '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
|
||||
10000: '0xD691F27f38B395864Ea86CfC7253969B409c362d',
|
||||
100000: undefined,
|
||||
},
|
||||
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||
symbol: 'USDC',
|
||||
decimals: 6,
|
||||
},
|
||||
cusdc: {
|
||||
instanceAddress: {
|
||||
5000: '0xaEaaC358560e11f52454D997AAFF2c5731B6f8a6',
|
||||
50000: '0x1356c899D8C9467C7f71C195612F8A395aBf2f0a',
|
||||
500000: '0xA60C772958a3eD56c1F15dD055bA37AC8e523a0D',
|
||||
5000000: undefined,
|
||||
},
|
||||
tokenAddress: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
symbol: 'cUSDC',
|
||||
decimals: 8,
|
||||
},
|
||||
usdt: {
|
||||
instanceAddress: {
|
||||
100: '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
|
||||
1000: '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
|
||||
10000: '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a',
|
||||
100000: '0x9AD122c22B14202B4490eDAf288FDb3C7cb3ff5E',
|
||||
},
|
||||
tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
||||
symbol: 'USDT',
|
||||
decimals: 6,
|
||||
},
|
||||
},
|
||||
netId42: {
|
||||
eth: {
|
||||
instanceAddress: {
|
||||
0.1: '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
|
||||
1: '0xD6a6AC46d02253c938B96D12BE439F570227aE8E',
|
||||
10: '0xe1BE96331391E519471100c3c1528B66B8F4e5a7',
|
||||
100: '0xd037E0Ac98Dab2fCb7E296c69C6e52767Ae5414D',
|
||||
},
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
dai: {
|
||||
instanceAddress: {
|
||||
100: '0xdf2d3cC5F361CF95b3f62c4bB66deFe3FDE47e3D',
|
||||
1000: '0xD96291dFa35d180a71964D0894a1Ae54247C4ccD',
|
||||
10000: '0xb192794f72EA45e33C3DF6fe212B9c18f6F45AE3',
|
||||
100000: undefined,
|
||||
},
|
||||
tokenAddress: '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa',
|
||||
symbol: 'DAI',
|
||||
decimals: 18,
|
||||
},
|
||||
cdai: {
|
||||
instanceAddress: {
|
||||
5000: '0x6Fc9386ABAf83147b3a89C36D422c625F44121C8',
|
||||
50000: '0x7182EA067e0f050997444FCb065985Fd677C16b6',
|
||||
500000: '0xC22ceFd90fbd1FdEeE554AE6Cc671179BC3b10Ae',
|
||||
5000000: undefined,
|
||||
},
|
||||
tokenAddress: '0xe7bc397DBd069fC7d0109C0636d06888bb50668c',
|
||||
symbol: 'cDAI',
|
||||
decimals: 8,
|
||||
},
|
||||
usdc: {
|
||||
instanceAddress: {
|
||||
100: '0x137E2B6d185018e7f09f6cf175a970e7fC73826C',
|
||||
1000: '0xcC7f1633A5068E86E3830e692e3e3f8f520525Af',
|
||||
10000: '0x28C8f149a0ab8A9bdB006B8F984fFFCCE52ef5EF',
|
||||
100000: undefined,
|
||||
},
|
||||
tokenAddress: '0x75B0622Cec14130172EaE9Cf166B92E5C112FaFF',
|
||||
symbol: 'USDC',
|
||||
decimals: 6,
|
||||
},
|
||||
cusdc: {
|
||||
instanceAddress: {
|
||||
5000: '0xc0648F28ABA385c8a1421Bbf1B59e3c474F89cB0',
|
||||
50000: '0x0C53853379c6b1A7B74E0A324AcbDD5Eabd4981D',
|
||||
500000: '0xf84016A0E03917cBe700D318EB1b7a53e6e3dEe1',
|
||||
5000000: undefined,
|
||||
},
|
||||
tokenAddress: '0xcfC9bB230F00bFFDB560fCe2428b4E05F3442E35',
|
||||
symbol: 'cUSDC',
|
||||
decimals: 8,
|
||||
},
|
||||
usdt: {
|
||||
instanceAddress: {
|
||||
100: '0x327853Da7916a6A0935563FB1919A48843036b42',
|
||||
1000: '0x531AA4DF5858EA1d0031Dad16e3274609DE5AcC0',
|
||||
10000: '0x0958275F0362cf6f07D21373aEE0cf37dFe415dD',
|
||||
100000: '0x14aEd24B67EaF3FF28503eB92aeb217C47514364',
|
||||
},
|
||||
tokenAddress: '0x03c5F29e9296006876d8DF210BCFfD7EA5Db1Cf1',
|
||||
symbol: 'USDT',
|
||||
decimals: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
const fs = require('fs')
|
||||
const assert = require('assert')
|
||||
const { bigInt } = require('snarkjs')
|
||||
const crypto = require('crypto')
|
||||
const circomlib = require('circomlib')
|
||||
const merkleTree = require('fixed-merkle-tree')
|
||||
const Web3 = require('web3')
|
||||
const buildGroth16 = require('websnark/src/groth16')
|
||||
const websnarkUtils = require('websnark/src/utils')
|
||||
const { toWei } = require('web3-utils')
|
||||
|
||||
let web3, contract, netId, circuit, proving_key, groth16
|
||||
const MERKLE_TREE_HEIGHT = 20
|
||||
const RPC_URL = 'https://kovan.infura.io/v3/0279e3bdf3ee49d0b547c643c2ef78ef'
|
||||
const PRIVATE_KEY = 'ad5b6eb7ee88173fa43dedcff8b1d9024d03f6307a1143ecf04bea8ed40f283f' // 0x94462e71A887756704f0fb1c0905264d487972fE
|
||||
const CONTRACT_ADDRESS = '0xD6a6AC46d02253c938B96D12BE439F570227aE8E'
|
||||
const AMOUNT = '1'
|
||||
// CURRENCY = 'ETH'
|
||||
|
||||
/** Generate random number of specified byte length */
|
||||
const rbigint = (nbytes) => bigInt.leBuff2int(crypto.randomBytes(nbytes))
|
||||
|
||||
/** Compute pedersen hash */
|
||||
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
|
||||
|
||||
/** BigNumber to hex string of specified length */
|
||||
const toHex = (number, length = 32) =>
|
||||
'0x' +
|
||||
(number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)).padStart(length * 2, '0')
|
||||
|
||||
/**
|
||||
* Create deposit object from secret and nullifier
|
||||
*/
|
||||
function createDeposit(nullifier, secret) {
|
||||
let deposit = { nullifier, secret }
|
||||
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
|
||||
deposit.commitment = pedersenHash(deposit.preimage)
|
||||
deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
|
||||
return deposit
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an ETH deposit
|
||||
*/
|
||||
async function deposit() {
|
||||
const deposit = createDeposit(rbigint(31), rbigint(31))
|
||||
console.log('Sending deposit transaction...')
|
||||
const tx = await contract.methods
|
||||
.deposit(toHex(deposit.commitment))
|
||||
.send({ value: toWei(AMOUNT), from: web3.eth.defaultAccount, gas: 2e6 })
|
||||
console.log(`https://kovan.etherscan.io/tx/${tx.transactionHash}`)
|
||||
return `tornado-eth-${AMOUNT}-${netId}-${toHex(deposit.preimage, 62)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an ETH withdrawal
|
||||
* @param note Note to withdraw
|
||||
* @param recipient Recipient address
|
||||
*/
|
||||
async function withdraw(note, recipient) {
|
||||
const deposit = parseNote(note)
|
||||
const { proof, args } = await generateSnarkProof(deposit, recipient)
|
||||
console.log('Sending withdrawal transaction...')
|
||||
const tx = await contract.methods.withdraw(proof, ...args).send({ from: web3.eth.defaultAccount, gas: 1e6 })
|
||||
console.log(`https://kovan.etherscan.io/tx/${tx.transactionHash}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Tornado.cash note
|
||||
* @param noteString the note
|
||||
*/
|
||||
function parseNote(noteString) {
|
||||
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g
|
||||
const match = noteRegex.exec(noteString)
|
||||
|
||||
// we are ignoring `currency`, `amount`, and `netId` for this minimal example
|
||||
const buf = Buffer.from(match.groups.note, 'hex')
|
||||
const nullifier = bigInt.leBuff2int(buf.slice(0, 31))
|
||||
const secret = bigInt.leBuff2int(buf.slice(31, 62))
|
||||
return createDeposit(nullifier, secret)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate merkle tree for a deposit.
|
||||
* Download deposit events from the contract, reconstructs merkle tree, finds our deposit leaf
|
||||
* in it and generates merkle proof
|
||||
* @param deposit Deposit object
|
||||
*/
|
||||
async function generateMerkleProof(deposit) {
|
||||
console.log('Getting contract state...')
|
||||
const events = await contract.getPastEvents('Deposit', { fromBlock: 0, toBlock: 'latest' })
|
||||
const leaves = events
|
||||
.sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order
|
||||
.map((e) => e.returnValues.commitment)
|
||||
const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves)
|
||||
|
||||
// Find current commitment in the tree
|
||||
let depositEvent = events.find((e) => e.returnValues.commitment === toHex(deposit.commitment))
|
||||
let leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
|
||||
|
||||
// Validate that our data is correct (optional)
|
||||
const isValidRoot = await contract.methods.isKnownRoot(toHex(tree.root())).call()
|
||||
const isSpent = await contract.methods.isSpent(toHex(deposit.nullifierHash)).call()
|
||||
assert(isValidRoot === true, 'Merkle tree is corrupted')
|
||||
assert(isSpent === false, 'The note is already spent')
|
||||
assert(leafIndex >= 0, 'The deposit is not found in the tree')
|
||||
|
||||
// Compute merkle proof of our commitment
|
||||
const { pathElements, pathIndices } = tree.path(leafIndex)
|
||||
return { pathElements, pathIndices, root: tree.root() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SNARK proof for withdrawal
|
||||
* @param deposit Deposit object
|
||||
* @param recipient Funds recipient
|
||||
*/
|
||||
async function generateSnarkProof(deposit, recipient) {
|
||||
// Compute merkle proof of our commitment
|
||||
const { root, pathElements, pathIndices } = await generateMerkleProof(deposit)
|
||||
|
||||
// Prepare circuit input
|
||||
const input = {
|
||||
// Public snark inputs
|
||||
root: root,
|
||||
nullifierHash: deposit.nullifierHash,
|
||||
recipient: bigInt(recipient),
|
||||
relayer: 0,
|
||||
fee: 0,
|
||||
refund: 0,
|
||||
|
||||
// Private snark inputs
|
||||
nullifier: deposit.nullifier,
|
||||
secret: deposit.secret,
|
||||
pathElements: pathElements,
|
||||
pathIndices: pathIndices,
|
||||
}
|
||||
|
||||
console.log('Generating SNARK proof...')
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const args = [
|
||||
toHex(input.root),
|
||||
toHex(input.nullifierHash),
|
||||
toHex(input.recipient, 20),
|
||||
toHex(input.relayer, 20),
|
||||
toHex(input.fee),
|
||||
toHex(input.refund),
|
||||
]
|
||||
|
||||
return { proof, args }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
web3 = new Web3(new Web3.providers.HttpProvider(RPC_URL, { timeout: 5 * 60 * 1000 }), null, {
|
||||
transactionConfirmationBlocks: 1,
|
||||
})
|
||||
circuit = require(__dirname + '/../build/circuits/withdraw.json')
|
||||
proving_key = fs.readFileSync(__dirname + '/../build/circuits/withdraw_proving_key.bin').buffer
|
||||
groth16 = await buildGroth16()
|
||||
netId = await web3.eth.net.getId()
|
||||
contract = new web3.eth.Contract(require('../build/contracts/ETHTornado.json').abi, CONTRACT_ADDRESS)
|
||||
const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY)
|
||||
web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
web3.eth.defaultAccount = account.address
|
||||
|
||||
const note = await deposit()
|
||||
console.log('Deposited note:', note)
|
||||
await withdraw(note, web3.eth.defaultAccount)
|
||||
console.log('Done')
|
||||
process.exit()
|
||||
}
|
||||
|
||||
main()
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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/c7463beadf2144e68646ff049917b716'),
|
||||
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/c7463beadf2144e68646ff049917b716'),
|
||||
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.11', // Fetch exact version from solc-bin (default: truffle's version)
|
||||
// docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
|
||||
settings: { // See the solidity docs for advice about optimization and evmVersion
|
||||
version: '0.7.6',
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
runs: 200
|
||||
runs: 200,
|
||||
},
|
||||
// evmVersion: "byzantium"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
external: {
|
||||
command: 'node ./scripts/compileHasher.js',
|
||||
targets: [
|
||||
{
|
||||
path: './build/Hasher.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
plugins: ['solidity-coverage'],
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue