.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tornado Cash Privacy Solution [](https://github.com/tornadocash/tornado-core/actions/workflows/build.yml) [](https://coveralls.io/github/tornadocash/tornado-core?branch=master)
2 |
3 | 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.
4 |
5 | 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.
6 |
7 | 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.
8 |
9 | You can read more about it in [this Medium article](https://medium.com/@tornado.cash/introducing-private-transactions-on-ethereum-now-42ee915babe0)
10 |
11 | ## Specs
12 |
13 | - Deposit gas cost: 1088354 (43381 + 50859 \* tree_depth)
14 | - Withdraw gas cost: 301233
15 | - Circuit Constraints = 28271 (1869 + 1325 \* tree_depth)
16 | - Circuit Proof time = 10213ms (1071 + 347 \* tree_depth)
17 | - Serverless
18 |
19 | 
20 |
21 | ## Whitepaper
22 |
23 | **[TornadoCash_whitepaper_v1.4.pdf](https://tornado.cash/audits/TornadoCash_whitepaper_v1.4.pdf)**
24 |
25 | ## Was it audited?
26 |
27 | 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.
28 |
29 | During the audit, no critical issues were found and all outstanding issues were fixed. The results can be found here:
30 |
31 | - Cryptographic review https://tornado.cash/audits/TornadoCash_cryptographic_review_ABDK.pdf
32 | - Smart contract audit https://tornado.cash/audits/TornadoCash_contract_audit_ABDK.pdf
33 | - Zk-SNARK circuits audit https://tornado.cash/audits/TornadoCash_circuit_audit_ABDK.pdf
34 |
35 | Underlying circomlib dependency is currently being audited, and the team already published most of the fixes for found issues
36 |
37 | ## Requirements
38 |
39 | 1. `node v11.15.0`
40 | 2. `npm install -g npx`
41 |
42 | ## Usage
43 |
44 | You can see example usage in cli.js, it works both in the console and in the browser.
45 |
46 | 1. `npm install`
47 | 1. `cp .env.example .env`
48 | 1. `npm run build` - this may take 10 minutes or more
49 | 1. `npx ganache-cli`
50 | 1. `npm run test` - optionally runs tests. It may fail on the first try, just run it again.
51 |
52 | Use browser version on Kovan:
53 |
54 | 1. `vi .env` - add your Kovan private key to deploy contracts
55 | 1. `npm run migrate`
56 | 1. `npx http-server` - serve current dir, you can use any other static http server
57 | 1. Open `localhost:8080`
58 |
59 | Use the command-line version. Works for Ganache, Kovan, and Mainnet:
60 |
61 | ### Initialization
62 |
63 | 1. `cp .env.example .env`
64 | 1. `npm run download`
65 | 1. `npm run build:contract`
66 |
67 | ### Ganache
68 |
69 | 1. make sure you complete steps from Initialization
70 | 1. `ganache-cli -i 1337`
71 | 1. `npm run migrate:dev`
72 | 1. `./cli.js test`
73 | 1. `./cli.js --help`
74 |
75 | ### Kovan, Mainnet
76 |
77 | 1. Please use https://github.com/tornadocash/tornado-cli
78 | 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
79 |
80 | Example:
81 |
82 | ```bash
83 | ./cli.js deposit ETH 0.1 --rpc https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448
84 | ```
85 |
86 | > Your note: tornado-eth-0.1-42-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652
87 | > Tornado ETH balance is 8.9
88 | > Sender account ETH balance is 1004873.470619891361352542
89 | > Submitting deposit transaction
90 | > Tornado ETH balance is 9
91 | > Sender account ETH balance is 1004873.361652048361352542
92 |
93 | ```bash
94 | ./cli.js withdraw tornado-eth-0.1-42-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 0x8589427373D6D84E98730D7795D8f6f8731FDA16 --rpc https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448 --relayer https://kovan-frelay.duckdns.org
95 | ```
96 |
97 | > Relay address: 0x6A31736e7490AbE5D5676be059DFf064AB4aC754
98 | > Getting current state from tornado contract
99 | > Generating SNARK proof
100 | > Proof time: 9117.051ms
101 | > Sending withdraw transaction through the relay
102 | > Transaction submitted through the relay. View transaction on etherscan https://kovan.etherscan.io/tx/0xcb21ae8cad723818c6bc7273e83e00c8393fcdbe74802ce5d562acad691a2a7b
103 | > Transaction mined in block 17036120
104 | > Done
105 |
106 | ## Deploy ETH Tornado Cash
107 |
108 | 1. `cp .env.example .env`
109 | 1. Tune all necessary params
110 | 1. `npx truffle migrate --network kovan --reset --f 2 --to 4`
111 |
112 | ## Deploy ERC20 Tornado Cash
113 |
114 | 1. `cp .env.example .env`
115 | 1. Tune all necessary params
116 | 1. `npx truffle migrate --network kovan --reset --f 2 --to 3`
117 | 1. `npx truffle migrate --network kovan --reset --f 5`
118 |
119 | **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`).
120 |
121 | ## How to resolve ENS name to DNS name for a relayer
122 |
123 | 1. Visit https://etherscan.io/enslookup and put relayer ENS name to the form.
124 | 2. Copy the namehash (1) and click on the `Resolver` link (2)
125 | 
126 | 3. Go to the `Contract` tab. Click on `Read Contract` and scroll down to the `5. text` method.
127 | 4. Put the values:
128 | 
129 | 5. Click `Query` and you will get the DNS name. Just add `https://` to it and use it as `relayer url`
130 |
131 | ## Credits
132 |
133 | Special thanks to @barryWhiteHat and @kobigurk for valuable input,
134 | and @jbaylina for awesome [Circom](https://github.com/iden3/circom) & [Websnark](https://github.com/iden3/websnark) framework
135 |
136 | ## Minimal demo example
137 |
138 | 1. `npm i`
139 | 1. `ganache-cli -d`
140 | 1. `npm run download`
141 | 1. `npm run build:contract`
142 | 1. `cp .env.example .env`
143 | 1. `npm run migrate:dev`
144 | 1. `node minimal-demo.js`
145 |
146 | ## Run tests/coverage
147 |
148 | Prepare test environment:
149 |
150 | ```
151 | yarn install
152 | yarn download
153 | cp .env.example .env
154 | npx ganache-cli > /dev/null &
155 | npm run migrate:dev
156 | ```
157 |
158 | Run tests:
159 |
160 | ```
161 | yarn test
162 | ```
163 |
164 | Run coverage:
165 |
166 | ```
167 | yarn coverage
168 | ```
169 |
170 | ## Emulate MPC trusted setup ceremony
171 |
172 | ```bash
173 | cargo install zkutil
174 | npx circom circuits/withdraw.circom -o build/circuits/withdraw.json
175 | zkutil setup -c build/circuits/withdraw.json -p build/circuits/withdraw.params
176 | 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
177 | zkutil generate-verifier -p build/circuits/withdraw.params -v build/circuits/Verifier.sol
178 | sed -i -e 's/pragma solidity \^0.6.0/pragma solidity 0.5.17/g' ./build/circuits/Verifier.sol
179 | ```
180 |
--------------------------------------------------------------------------------
/circuits/merkleTree.circom:
--------------------------------------------------------------------------------
1 | include "../node_modules/circomlib/circuits/mimcsponge.circom";
2 |
3 | // Computes MiMC([left, right])
4 | template HashLeftRight() {
5 | signal input left;
6 | signal input right;
7 | signal output hash;
8 |
9 | component hasher = MiMCSponge(2, 1);
10 | hasher.ins[0] <== left;
11 | hasher.ins[1] <== right;
12 | hasher.k <== 0;
13 | hash <== hasher.outs[0];
14 | }
15 |
16 | // if s == 0 returns [in[0], in[1]]
17 | // if s == 1 returns [in[1], in[0]]
18 | template DualMux() {
19 | signal input in[2];
20 | signal input s;
21 | signal output out[2];
22 |
23 | s * (1 - s) === 0
24 | out[0] <== (in[1] - in[0])*s + in[0];
25 | out[1] <== (in[0] - in[1])*s + in[1];
26 | }
27 |
28 | // Verifies that merkle proof is correct for given merkle root and a leaf
29 | // pathIndices input is an array of 0/1 selectors telling whether given pathElement is on the left or right side of merkle path
30 | template MerkleTreeChecker(levels) {
31 | signal input leaf;
32 | signal input root;
33 | signal input pathElements[levels];
34 | signal input pathIndices[levels];
35 |
36 | component selectors[levels];
37 | component hashers[levels];
38 |
39 | for (var i = 0; i < levels; i++) {
40 | selectors[i] = DualMux();
41 | selectors[i].in[0] <== i == 0 ? leaf : hashers[i - 1].hash;
42 | selectors[i].in[1] <== pathElements[i];
43 | selectors[i].s <== pathIndices[i];
44 |
45 | hashers[i] = HashLeftRight();
46 | hashers[i].left <== selectors[i].out[0];
47 | hashers[i].right <== selectors[i].out[1];
48 | }
49 |
50 | root === hashers[levels - 1].hash;
51 | }
52 |
--------------------------------------------------------------------------------
/circuits/withdraw.circom:
--------------------------------------------------------------------------------
1 | include "../node_modules/circomlib/circuits/bitify.circom";
2 | include "../node_modules/circomlib/circuits/pedersen.circom";
3 | include "merkleTree.circom";
4 |
5 | // computes Pedersen(nullifier + secret)
6 | template CommitmentHasher() {
7 | signal input nullifier;
8 | signal input secret;
9 | signal output commitment;
10 | signal output nullifierHash;
11 |
12 | component commitmentHasher = Pedersen(496);
13 | component nullifierHasher = Pedersen(248);
14 | component nullifierBits = Num2Bits(248);
15 | component secretBits = Num2Bits(248);
16 | nullifierBits.in <== nullifier;
17 | secretBits.in <== secret;
18 | for (var i = 0; i < 248; i++) {
19 | nullifierHasher.in[i] <== nullifierBits.out[i];
20 | commitmentHasher.in[i] <== nullifierBits.out[i];
21 | commitmentHasher.in[i + 248] <== secretBits.out[i];
22 | }
23 |
24 | commitment <== commitmentHasher.out[0];
25 | nullifierHash <== nullifierHasher.out[0];
26 | }
27 |
28 | // Verifies that commitment that corresponds to given secret and nullifier is included in the merkle tree of deposits
29 | template Withdraw(levels) {
30 | signal input root;
31 | signal input nullifierHash;
32 | signal input recipient; // not taking part in any computations
33 | signal input relayer; // not taking part in any computations
34 | signal input fee; // not taking part in any computations
35 | signal input refund; // not taking part in any computations
36 | signal private input nullifier;
37 | signal private input secret;
38 | signal private input pathElements[levels];
39 | signal private input pathIndices[levels];
40 |
41 | component hasher = CommitmentHasher();
42 | hasher.nullifier <== nullifier;
43 | hasher.secret <== secret;
44 | hasher.nullifierHash === nullifierHash;
45 |
46 | component tree = MerkleTreeChecker(levels);
47 | tree.leaf <== hasher.commitment;
48 | tree.root <== root;
49 | for (var i = 0; i < levels; i++) {
50 | tree.pathElements[i] <== pathElements[i];
51 | tree.pathIndices[i] <== pathIndices[i];
52 | }
53 |
54 | // Add hidden signals to make sure that tampering with recipient or fee will invalidate the snark proof
55 | // Most likely it is not required, but it's better to stay on the safe side and it only takes 2 constraints
56 | // Squares are used to prevent optimizer from removing those constraints
57 | signal recipientSquare;
58 | signal feeSquare;
59 | signal relayerSquare;
60 | signal refundSquare;
61 | recipientSquare <== recipient * recipient;
62 | feeSquare <== fee * fee;
63 | relayerSquare <== relayer * relayer;
64 | refundSquare <== refund * refund;
65 | }
66 |
67 | component main = Withdraw(20);
68 |
--------------------------------------------------------------------------------
/contracts/ERC20Tornado.sol:
--------------------------------------------------------------------------------
1 | // https://tornado.cash
2 | /*
3 | * d888888P dP a88888b. dP
4 | * 88 88 d8' `88 88
5 | * 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
6 | * 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
7 | * 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
8 | * dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
9 | * ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
10 | */
11 |
12 | // SPDX-License-Identifier: MIT
13 | pragma solidity ^0.7.0;
14 |
15 | import "./Tornado.sol";
16 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
17 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
18 |
19 | contract ERC20Tornado is Tornado {
20 | using SafeERC20 for IERC20;
21 | IERC20 public token;
22 |
23 | constructor(
24 | IVerifier _verifier,
25 | IHasher _hasher,
26 | uint256 _denomination,
27 | uint32 _merkleTreeHeight,
28 | IERC20 _token
29 | ) Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight) {
30 | token = _token;
31 | }
32 |
33 | function _processDeposit() internal override {
34 | require(msg.value == 0, "ETH value is supposed to be 0 for ERC20 instance");
35 | token.safeTransferFrom(msg.sender, address(this), denomination);
36 | }
37 |
38 | function _processWithdraw(
39 | address payable _recipient,
40 | address payable _relayer,
41 | uint256 _fee,
42 | uint256 _refund
43 | ) internal override {
44 | require(msg.value == _refund, "Incorrect refund amount received by the contract");
45 |
46 | token.safeTransfer(_recipient, denomination - _fee);
47 | if (_fee > 0) {
48 | token.safeTransfer(_relayer, _fee);
49 | }
50 |
51 | if (_refund > 0) {
52 | (bool success, ) = _recipient.call{ value: _refund }("");
53 | if (!success) {
54 | // let's return _refund back to the relayer
55 | _relayer.transfer(_refund);
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/contracts/ETHTornado.sol:
--------------------------------------------------------------------------------
1 | // https://tornado.cash
2 | /*
3 | * d888888P dP a88888b. dP
4 | * 88 88 d8' `88 88
5 | * 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
6 | * 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
7 | * 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
8 | * dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
9 | * ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
10 | */
11 |
12 | // SPDX-License-Identifier: MIT
13 | pragma solidity ^0.7.0;
14 |
15 | import "./Tornado.sol";
16 |
17 | contract ETHTornado is Tornado {
18 | constructor(
19 | IVerifier _verifier,
20 | IHasher _hasher,
21 | uint256 _denomination,
22 | uint32 _merkleTreeHeight
23 | ) Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight) {}
24 |
25 | function _processDeposit() internal override {
26 | require(msg.value == denomination, "Please send `mixDenomination` ETH along with transaction");
27 | }
28 |
29 | function _processWithdraw(
30 | address payable _recipient,
31 | address payable _relayer,
32 | uint256 _fee,
33 | uint256 _refund
34 | ) internal override {
35 | // sanity checks
36 | require(msg.value == 0, "Message value is supposed to be zero for ETH instance");
37 | require(_refund == 0, "Refund value is supposed to be zero for ETH instance");
38 |
39 | (bool success, ) = _recipient.call{ value: denomination - _fee }("");
40 | require(success, "payment to _recipient did not go thru");
41 | if (_fee > 0) {
42 | (success, ) = _relayer.call{ value: _fee }("");
43 | require(success, "payment to _relayer did not go thru");
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/contracts/MerkleTreeWithHistory.sol:
--------------------------------------------------------------------------------
1 | // https://tornado.cash
2 | /*
3 | * d888888P dP a88888b. dP
4 | * 88 88 d8' `88 88
5 | * 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
6 | * 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
7 | * 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
8 | * dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
9 | * ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
10 | */
11 |
12 | // SPDX-License-Identifier: MIT
13 | pragma solidity ^0.7.0;
14 |
15 | interface IHasher {
16 | function MiMCSponge(uint256 in_xL, uint256 in_xR) external pure returns (uint256 xL, uint256 xR);
17 | }
18 |
19 | contract MerkleTreeWithHistory {
20 | uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
21 | uint256 public constant ZERO_VALUE = 21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE
22 | IHasher public immutable hasher;
23 |
24 | uint32 public levels;
25 |
26 | // the following variables are made public for easier testing and debugging and
27 | // are not supposed to be accessed in regular code
28 |
29 | // filledSubtrees and roots could be bytes32[size], but using mappings makes it cheaper because
30 | // it removes index range check on every interaction
31 | mapping(uint256 => bytes32) public filledSubtrees;
32 | mapping(uint256 => bytes32) public roots;
33 | uint32 public constant ROOT_HISTORY_SIZE = 30;
34 | uint32 public currentRootIndex = 0;
35 | uint32 public nextIndex = 0;
36 |
37 | constructor(uint32 _levels, IHasher _hasher) {
38 | require(_levels > 0, "_levels should be greater than zero");
39 | require(_levels < 32, "_levels should be less than 32");
40 | levels = _levels;
41 | hasher = _hasher;
42 |
43 | for (uint32 i = 0; i < _levels; i++) {
44 | filledSubtrees[i] = zeros(i);
45 | }
46 |
47 | roots[0] = zeros(_levels - 1);
48 | }
49 |
50 | /**
51 | @dev Hash 2 tree leaves, returns MiMC(_left, _right)
52 | */
53 | function hashLeftRight(
54 | IHasher _hasher,
55 | bytes32 _left,
56 | bytes32 _right
57 | ) public pure returns (bytes32) {
58 | require(uint256(_left) < FIELD_SIZE, "_left should be inside the field");
59 | require(uint256(_right) < FIELD_SIZE, "_right should be inside the field");
60 | uint256 R = uint256(_left);
61 | uint256 C = 0;
62 | (R, C) = _hasher.MiMCSponge(R, C);
63 | R = addmod(R, uint256(_right), FIELD_SIZE);
64 | (R, C) = _hasher.MiMCSponge(R, C);
65 | return bytes32(R);
66 | }
67 |
68 | function _insert(bytes32 _leaf) internal returns (uint32 index) {
69 | uint32 _nextIndex = nextIndex;
70 | require(_nextIndex != uint32(2)**levels, "Merkle tree is full. No more leaves can be added");
71 | uint32 currentIndex = _nextIndex;
72 | bytes32 currentLevelHash = _leaf;
73 | bytes32 left;
74 | bytes32 right;
75 |
76 | for (uint32 i = 0; i < levels; i++) {
77 | if (currentIndex % 2 == 0) {
78 | left = currentLevelHash;
79 | right = zeros(i);
80 | filledSubtrees[i] = currentLevelHash;
81 | } else {
82 | left = filledSubtrees[i];
83 | right = currentLevelHash;
84 | }
85 | currentLevelHash = hashLeftRight(hasher, left, right);
86 | currentIndex /= 2;
87 | }
88 |
89 | uint32 newRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
90 | currentRootIndex = newRootIndex;
91 | roots[newRootIndex] = currentLevelHash;
92 | nextIndex = _nextIndex + 1;
93 | return _nextIndex;
94 | }
95 |
96 | /**
97 | @dev Whether the root is present in the root history
98 | */
99 | function isKnownRoot(bytes32 _root) public view returns (bool) {
100 | if (_root == 0) {
101 | return false;
102 | }
103 | uint32 _currentRootIndex = currentRootIndex;
104 | uint32 i = _currentRootIndex;
105 | do {
106 | if (_root == roots[i]) {
107 | return true;
108 | }
109 | if (i == 0) {
110 | i = ROOT_HISTORY_SIZE;
111 | }
112 | i--;
113 | } while (i != _currentRootIndex);
114 | return false;
115 | }
116 |
117 | /**
118 | @dev Returns the last root
119 | */
120 | function getLastRoot() public view returns (bytes32) {
121 | return roots[currentRootIndex];
122 | }
123 |
124 | /// @dev provides Zero (Empty) elements for a MiMC MerkleTree. Up to 32 levels
125 | function zeros(uint256 i) public pure returns (bytes32) {
126 | if (i == 0) return bytes32(0x2fe54c60d3acabf3343a35b6eba15db4821b340f76e741e2249685ed4899af6c);
127 | else if (i == 1) return bytes32(0x256a6135777eee2fd26f54b8b7037a25439d5235caee224154186d2b8a52e31d);
128 | else if (i == 2) return bytes32(0x1151949895e82ab19924de92c40a3d6f7bcb60d92b00504b8199613683f0c200);
129 | else if (i == 3) return bytes32(0x20121ee811489ff8d61f09fb89e313f14959a0f28bb428a20dba6b0b068b3bdb);
130 | else if (i == 4) return bytes32(0x0a89ca6ffa14cc462cfedb842c30ed221a50a3d6bf022a6a57dc82ab24c157c9);
131 | else if (i == 5) return bytes32(0x24ca05c2b5cd42e890d6be94c68d0689f4f21c9cec9c0f13fe41d566dfb54959);
132 | else if (i == 6) return bytes32(0x1ccb97c932565a92c60156bdba2d08f3bf1377464e025cee765679e604a7315c);
133 | else if (i == 7) return bytes32(0x19156fbd7d1a8bf5cba8909367de1b624534ebab4f0f79e003bccdd1b182bdb4);
134 | else if (i == 8) return bytes32(0x261af8c1f0912e465744641409f622d466c3920ac6e5ff37e36604cb11dfff80);
135 | else if (i == 9) return bytes32(0x0058459724ff6ca5a1652fcbc3e82b93895cf08e975b19beab3f54c217d1c007);
136 | else if (i == 10) return bytes32(0x1f04ef20dee48d39984d8eabe768a70eafa6310ad20849d4573c3c40c2ad1e30);
137 | else if (i == 11) return bytes32(0x1bea3dec5dab51567ce7e200a30f7ba6d4276aeaa53e2686f962a46c66d511e5);
138 | else if (i == 12) return bytes32(0x0ee0f941e2da4b9e31c3ca97a40d8fa9ce68d97c084177071b3cb46cd3372f0f);
139 | else if (i == 13) return bytes32(0x1ca9503e8935884501bbaf20be14eb4c46b89772c97b96e3b2ebf3a36a948bbd);
140 | else if (i == 14) return bytes32(0x133a80e30697cd55d8f7d4b0965b7be24057ba5dc3da898ee2187232446cb108);
141 | else if (i == 15) return bytes32(0x13e6d8fc88839ed76e182c2a779af5b2c0da9dd18c90427a644f7e148a6253b6);
142 | else if (i == 16) return bytes32(0x1eb16b057a477f4bc8f572ea6bee39561098f78f15bfb3699dcbb7bd8db61854);
143 | else if (i == 17) return bytes32(0x0da2cb16a1ceaabf1c16b838f7a9e3f2a3a3088d9e0a6debaa748114620696ea);
144 | else if (i == 18) return bytes32(0x24a3b3d822420b14b5d8cb6c28a574f01e98ea9e940551d2ebd75cee12649f9d);
145 | else if (i == 19) return bytes32(0x198622acbd783d1b0d9064105b1fc8e4d8889de95c4c519b3f635809fe6afc05);
146 | else if (i == 20) return bytes32(0x29d7ed391256ccc3ea596c86e933b89ff339d25ea8ddced975ae2fe30b5296d4);
147 | else if (i == 21) return bytes32(0x19be59f2f0413ce78c0c3703a3a5451b1d7f39629fa33abd11548a76065b2967);
148 | else if (i == 22) return bytes32(0x1ff3f61797e538b70e619310d33f2a063e7eb59104e112e95738da1254dc3453);
149 | else if (i == 23) return bytes32(0x10c16ae9959cf8358980d9dd9616e48228737310a10e2b6b731c1a548f036c48);
150 | else if (i == 24) return bytes32(0x0ba433a63174a90ac20992e75e3095496812b652685b5e1a2eae0b1bf4e8fcd1);
151 | else if (i == 25) return bytes32(0x019ddb9df2bc98d987d0dfeca9d2b643deafab8f7036562e627c3667266a044c);
152 | else if (i == 26) return bytes32(0x2d3c88b23175c5a5565db928414c66d1912b11acf974b2e644caaac04739ce99);
153 | else if (i == 27) return bytes32(0x2eab55f6ae4e66e32c5189eed5c470840863445760f5ed7e7b69b2a62600f354);
154 | else if (i == 28) return bytes32(0x002df37a2642621802383cf952bf4dd1f32e05433beeb1fd41031fb7eace979d);
155 | else if (i == 29) return bytes32(0x104aeb41435db66c3e62feccc1d6f5d98d0a0ed75d1374db457cf462e3a1f427);
156 | else if (i == 30) return bytes32(0x1f3c6fd858e9a7d4b0d1f38e256a09d81d5a5e3c963987e2d4b814cfab7c6ebb);
157 | else if (i == 31) return bytes32(0x2c7a07d20dff79d01fecedc1134284a8d08436606c93693b67e333f671bf69cc);
158 | else revert("Index out of bounds");
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/contracts/Mocks/BadRecipient.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.7.0;
3 |
4 | contract BadRecipient {
5 | fallback() external {
6 | require(false, "this contract does not accept ETH");
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/contracts/Mocks/ERC20Mock.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.7.0;
3 |
4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5 |
6 | contract ERC20Mock is ERC20("DAIMock", "DAIM") {
7 | function mint(address account, uint256 amount) public {
8 | _mint(account, amount);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/contracts/Mocks/IDeployer.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.7.0;
3 |
4 | interface IDeployer {
5 | function deploy(bytes memory _initCode, bytes32 _salt) external returns (address payable createdContract);
6 | }
7 |
--------------------------------------------------------------------------------
/contracts/Mocks/IUSDT.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.7.0;
4 |
5 | interface ERC20Basic {
6 | function _totalSupply() external returns (uint256);
7 |
8 | function totalSupply() external view returns (uint256);
9 |
10 | function balanceOf(address who) external view returns (uint256);
11 |
12 | function transfer(address to, uint256 value) external;
13 |
14 | event Transfer(address indexed from, address indexed to, uint256 value);
15 | }
16 |
17 | /**
18 | * @title ERC20 interface
19 | * @dev see https://github.com/ethereum/EIPs/issues/20
20 | */
21 | interface IUSDT is ERC20Basic {
22 | function allowance(address owner, address spender) external view returns (uint256);
23 |
24 | function transferFrom(
25 | address from,
26 | address to,
27 | uint256 value
28 | ) external;
29 |
30 | function approve(address spender, uint256 value) external;
31 |
32 | event Approval(address indexed owner, address indexed spender, uint256 value);
33 | }
34 |
--------------------------------------------------------------------------------
/contracts/Mocks/MerkleTreeWithHistoryMock.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.7.0;
3 |
4 | import "../MerkleTreeWithHistory.sol";
5 |
6 | contract MerkleTreeWithHistoryMock is MerkleTreeWithHistory {
7 | constructor(uint32 _treeLevels, IHasher _hasher) MerkleTreeWithHistory(_treeLevels, _hasher) {}
8 |
9 | function insert(bytes32 _leaf) public {
10 | _insert(_leaf);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/contracts/Tornado.sol:
--------------------------------------------------------------------------------
1 | // https://tornado.cash
2 | /*
3 | * d888888P dP a88888b. dP
4 | * 88 88 d8' `88 88
5 | * 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
6 | * 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
7 | * 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
8 | * dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
9 | * ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
10 | */
11 |
12 | // SPDX-License-Identifier: MIT
13 | pragma solidity ^0.7.0;
14 |
15 | import "./MerkleTreeWithHistory.sol";
16 | import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
17 |
18 | interface IVerifier {
19 | function verifyProof(bytes memory _proof, uint256[6] memory _input) external returns (bool);
20 | }
21 |
22 | abstract contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
23 | IVerifier public immutable verifier;
24 | uint256 public denomination;
25 |
26 | mapping(bytes32 => bool) public nullifierHashes;
27 | // we store all commitments just to prevent accidental deposits with the same commitment
28 | mapping(bytes32 => bool) public commitments;
29 |
30 | event Deposit(bytes32 indexed commitment, uint32 leafIndex, uint256 timestamp);
31 | event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 fee);
32 |
33 | /**
34 | @dev The constructor
35 | @param _verifier the address of SNARK verifier for this contract
36 | @param _hasher the address of MiMC hash contract
37 | @param _denomination transfer amount for each deposit
38 | @param _merkleTreeHeight the height of deposits' Merkle Tree
39 | */
40 | constructor(
41 | IVerifier _verifier,
42 | IHasher _hasher,
43 | uint256 _denomination,
44 | uint32 _merkleTreeHeight
45 | ) MerkleTreeWithHistory(_merkleTreeHeight, _hasher) {
46 | require(_denomination > 0, "denomination should be greater than 0");
47 | verifier = _verifier;
48 | denomination = _denomination;
49 | }
50 |
51 | /**
52 | @dev Deposit funds into the contract. The caller must send (for ETH) or approve (for ERC20) value equal to or `denomination` of this instance.
53 | @param _commitment the note commitment, which is PedersenHash(nullifier + secret)
54 | */
55 | function deposit(bytes32 _commitment) external payable nonReentrant {
56 | require(!commitments[_commitment], "The commitment has been submitted");
57 |
58 | uint32 insertedIndex = _insert(_commitment);
59 | commitments[_commitment] = true;
60 | _processDeposit();
61 |
62 | emit Deposit(_commitment, insertedIndex, block.timestamp);
63 | }
64 |
65 | /** @dev this function is defined in a child contract */
66 | function _processDeposit() internal virtual;
67 |
68 | /**
69 | @dev Withdraw a deposit from the contract. `proof` is a zkSNARK proof data, and input is an array of circuit public inputs
70 | `input` array consists of:
71 | - merkle root of all deposits in the contract
72 | - hash of unique deposit nullifier to prevent double spends
73 | - the recipient of funds
74 | - optional fee that goes to the transaction sender (usually a relay)
75 | */
76 | function withdraw(
77 | bytes calldata _proof,
78 | bytes32 _root,
79 | bytes32 _nullifierHash,
80 | address payable _recipient,
81 | address payable _relayer,
82 | uint256 _fee,
83 | uint256 _refund
84 | ) external payable nonReentrant {
85 | require(_fee <= denomination, "Fee exceeds transfer value");
86 | require(!nullifierHashes[_nullifierHash], "The note has been already spent");
87 | require(isKnownRoot(_root), "Cannot find your merkle root"); // Make sure to use a recent one
88 | require(
89 | verifier.verifyProof(
90 | _proof,
91 | [uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund]
92 | ),
93 | "Invalid withdraw proof"
94 | );
95 |
96 | nullifierHashes[_nullifierHash] = true;
97 | _processWithdraw(_recipient, _relayer, _fee, _refund);
98 | emit Withdrawal(_recipient, _nullifierHash, _relayer, _fee);
99 | }
100 |
101 | /** @dev this function is defined in a child contract */
102 | function _processWithdraw(
103 | address payable _recipient,
104 | address payable _relayer,
105 | uint256 _fee,
106 | uint256 _refund
107 | ) internal virtual;
108 |
109 | /** @dev whether a note is already spent */
110 | function isSpent(bytes32 _nullifierHash) public view returns (bool) {
111 | return nullifierHashes[_nullifierHash];
112 | }
113 |
114 | /** @dev whether an array of notes is already spent */
115 | function isSpentArray(bytes32[] calldata _nullifierHashes) external view returns (bool[] memory spent) {
116 | spent = new bool[](_nullifierHashes.length);
117 | for (uint256 i = 0; i < _nullifierHashes.length; i++) {
118 | if (isSpent(_nullifierHashes[i])) {
119 | spent[i] = true;
120 | }
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/contracts/Verifier.sol:
--------------------------------------------------------------------------------
1 | /**
2 | *Submitted for verification at Etherscan.io on 2020-05-12
3 | */
4 |
5 | // https://tornado.cash Verifier.sol generated by trusted setup ceremony.
6 | /*
7 | * d888888P dP a88888b. dP
8 | * 88 88 d8' `88 88
9 | * 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
10 | * 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
11 | * 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
12 | * dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
13 | * ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
14 | */
15 | // SPDX-License-Identifier: MIT
16 | // Copyright 2017 Christian Reitwiessner
17 | // Permission is hereby granted, free of charge, to any person obtaining a copy
18 | // of this software and associated documentation files (the "Software"), to
19 | // deal in the Software without restriction, including without limitation the
20 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
21 | // sell copies of the Software, and to permit persons to whom the Software is
22 | // furnished to do so, subject to the following conditions:
23 | // The above copyright notice and this permission notice shall be included in
24 | // all copies or substantial portions of the Software.
25 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
30 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
31 | // IN THE SOFTWARE.
32 |
33 | // 2019 OKIMS
34 |
35 | pragma solidity ^0.7.0;
36 |
37 | library Pairing {
38 | uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
39 |
40 | struct G1Point {
41 | uint256 X;
42 | uint256 Y;
43 | }
44 |
45 | // Encoding of field elements is: X[0] * z + X[1]
46 | struct G2Point {
47 | uint256[2] X;
48 | uint256[2] Y;
49 | }
50 |
51 | /*
52 | * @return The negation of p, i.e. p.plus(p.negate()) should be zero.
53 | */
54 | function negate(G1Point memory p) internal pure returns (G1Point memory) {
55 | // The prime q in the base field F_q for G1
56 | if (p.X == 0 && p.Y == 0) {
57 | return G1Point(0, 0);
58 | } else {
59 | return G1Point(p.X, PRIME_Q - (p.Y % PRIME_Q));
60 | }
61 | }
62 |
63 | /*
64 | * @return r the sum of two points of G1
65 | */
66 | function plus(
67 | G1Point memory p1,
68 | G1Point memory p2
69 | ) internal view returns (G1Point memory r) {
70 | uint256[4] memory input;
71 | input[0] = p1.X;
72 | input[1] = p1.Y;
73 | input[2] = p2.X;
74 | input[3] = p2.Y;
75 | bool success;
76 |
77 | // solium-disable-next-line security/no-inline-assembly
78 | assembly {
79 | success := staticcall(sub(gas(), 2000), 6, input, 0xc0, r, 0x60)
80 | // Use "invalid" to make gas estimation work
81 | switch success case 0 { invalid() }
82 | }
83 |
84 | require(success, "pairing-add-failed");
85 | }
86 |
87 | /*
88 | * @return r the product of a point on G1 and a scalar, i.e.
89 | * p == p.scalar_mul(1) and p.plus(p) == p.scalar_mul(2) for all
90 | * points p.
91 | */
92 | function scalar_mul(G1Point memory p, uint256 s) internal view returns (G1Point memory r) {
93 | uint256[3] memory input;
94 | input[0] = p.X;
95 | input[1] = p.Y;
96 | input[2] = s;
97 | bool success;
98 | // solium-disable-next-line security/no-inline-assembly
99 | assembly {
100 | success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60)
101 | // Use "invalid" to make gas estimation work
102 | switch success case 0 { invalid() }
103 | }
104 | require(success, "pairing-mul-failed");
105 | }
106 |
107 | /* @return The result of computing the pairing check
108 | * e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1
109 | * For example,
110 | * pairing([P1(), P1().negate()], [P2(), P2()]) should return true.
111 | */
112 | function pairing(
113 | G1Point memory a1,
114 | G2Point memory a2,
115 | G1Point memory b1,
116 | G2Point memory b2,
117 | G1Point memory c1,
118 | G2Point memory c2,
119 | G1Point memory d1,
120 | G2Point memory d2
121 | ) internal view returns (bool) {
122 | G1Point[4] memory p1 = [a1, b1, c1, d1];
123 | G2Point[4] memory p2 = [a2, b2, c2, d2];
124 |
125 | uint256 inputSize = 24;
126 | uint256[] memory input = new uint256[](inputSize);
127 |
128 | for (uint256 i = 0; i < 4; i++) {
129 | uint256 j = i * 6;
130 | input[j + 0] = p1[i].X;
131 | input[j + 1] = p1[i].Y;
132 | input[j + 2] = p2[i].X[0];
133 | input[j + 3] = p2[i].X[1];
134 | input[j + 4] = p2[i].Y[0];
135 | input[j + 5] = p2[i].Y[1];
136 | }
137 |
138 | uint256[1] memory out;
139 | bool success;
140 |
141 | // solium-disable-next-line security/no-inline-assembly
142 | assembly {
143 | success := staticcall(sub(gas(), 2000), 8, add(input, 0x20), mul(inputSize, 0x20), out, 0x20)
144 | // Use "invalid" to make gas estimation work
145 | switch success case 0 { invalid() }
146 | }
147 |
148 | require(success, "pairing-opcode-failed");
149 |
150 | return out[0] != 0;
151 | }
152 | }
153 |
154 | contract Verifier {
155 | uint256 constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
156 | uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
157 | using Pairing for *;
158 |
159 | struct VerifyingKey {
160 | Pairing.G1Point alfa1;
161 | Pairing.G2Point beta2;
162 | Pairing.G2Point gamma2;
163 | Pairing.G2Point delta2;
164 | Pairing.G1Point[7] IC;
165 | }
166 |
167 | struct Proof {
168 | Pairing.G1Point A;
169 | Pairing.G2Point B;
170 | Pairing.G1Point C;
171 | }
172 |
173 | function verifyingKey() internal pure returns (VerifyingKey memory vk) {
174 | vk.alfa1 = Pairing.G1Point(uint256(20692898189092739278193869274495556617788530808486270118371701516666252877969), uint256(11713062878292653967971378194351968039596396853904572879488166084231740557279));
175 | vk.beta2 = Pairing.G2Point([uint256(12168528810181263706895252315640534818222943348193302139358377162645029937006), uint256(281120578337195720357474965979947690431622127986816839208576358024608803542)], [uint256(16129176515713072042442734839012966563817890688785805090011011570989315559913), uint256(9011703453772030375124466642203641636825223906145908770308724549646909480510)]);
176 | vk.gamma2 = Pairing.G2Point([uint256(11559732032986387107991004021392285783925812861821192530917403151452391805634), uint256(10857046999023057135944570762232829481370756359578518086990519993285655852781)], [uint256(4082367875863433681332203403145435568316851327593401208105741076214120093531), uint256(8495653923123431417604973247489272438418190587263600148770280649306958101930)]);
177 | vk.delta2 = Pairing.G2Point([uint256(21280594949518992153305586783242820682644996932183186320680800072133486887432), uint256(150879136433974552800030963899771162647715069685890547489132178314736470662)], [uint256(1081836006956609894549771334721413187913047383331561601606260283167615953295), uint256(11434086686358152335540554643130007307617078324975981257823476472104616196090)]);
178 | vk.IC[0] = Pairing.G1Point(uint256(16225148364316337376768119297456868908427925829817748684139175309620217098814), uint256(5167268689450204162046084442581051565997733233062478317813755636162413164690));
179 | vk.IC[1] = Pairing.G1Point(uint256(12882377842072682264979317445365303375159828272423495088911985689463022094260), uint256(19488215856665173565526758360510125932214252767275816329232454875804474844786));
180 | vk.IC[2] = Pairing.G1Point(uint256(13083492661683431044045992285476184182144099829507350352128615182516530014777), uint256(602051281796153692392523702676782023472744522032670801091617246498551238913));
181 | vk.IC[3] = Pairing.G1Point(uint256(9732465972180335629969421513785602934706096902316483580882842789662669212890), uint256(2776526698606888434074200384264824461688198384989521091253289776235602495678));
182 | vk.IC[4] = Pairing.G1Point(uint256(8586364274534577154894611080234048648883781955345622578531233113180532234842), uint256(21276134929883121123323359450658320820075698490666870487450985603988214349407));
183 | vk.IC[5] = Pairing.G1Point(uint256(4910628533171597675018724709631788948355422829499855033965018665300386637884), uint256(20532468890024084510431799098097081600480376127870299142189696620752500664302));
184 | vk.IC[6] = Pairing.G1Point(uint256(15335858102289947642505450692012116222827233918185150176888641903531542034017), uint256(5311597067667671581646709998171703828965875677637292315055030353779531404812));
185 |
186 | }
187 |
188 | /*
189 | * @returns Whether the proof is valid given the hardcoded verifying key
190 | * above and the public inputs
191 | */
192 | function verifyProof(
193 | bytes memory proof,
194 | uint256[6] memory input
195 | ) public view returns (bool) {
196 | uint256[8] memory p = abi.decode(proof, (uint256[8]));
197 |
198 | // Make sure that each element in the proof is less than the prime q
199 | for (uint8 i = 0; i < p.length; i++) {
200 | require(p[i] < PRIME_Q, "verifier-proof-element-gte-prime-q");
201 | }
202 |
203 | Proof memory _proof;
204 | _proof.A = Pairing.G1Point(p[0], p[1]);
205 | _proof.B = Pairing.G2Point([p[2], p[3]], [p[4], p[5]]);
206 | _proof.C = Pairing.G1Point(p[6], p[7]);
207 |
208 | VerifyingKey memory vk = verifyingKey();
209 |
210 | // Compute the linear combination vk_x
211 | Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0);
212 | vk_x = Pairing.plus(vk_x, vk.IC[0]);
213 |
214 | // Make sure that every input is less than the snark scalar field
215 | for (uint256 i = 0; i < input.length; i++) {
216 | require(input[i] < SNARK_SCALAR_FIELD, "verifier-gte-snark-scalar-field");
217 | vk_x = Pairing.plus(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i]));
218 | }
219 |
220 | return Pairing.pairing(
221 | Pairing.negate(_proof.A),
222 | _proof.B,
223 | vk.alfa1,
224 | vk.beta2,
225 | vk_x,
226 | vk.gamma2,
227 | _proof.C,
228 | vk.delta2
229 | );
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/contracts/cTornado.sol:
--------------------------------------------------------------------------------
1 | // https://tornado.cash
2 | /*
3 | * d888888P dP a88888b. dP
4 | * 88 88 d8' `88 88
5 | * 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
6 | * 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
7 | * 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
8 | * dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
9 | * ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
10 | */
11 |
12 | // SPDX-License-Identifier: MIT
13 | pragma solidity ^0.7.0;
14 |
15 | import "./ERC20Tornado.sol";
16 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
17 |
18 | contract cTornado is ERC20Tornado {
19 | address public immutable governance = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
20 | IERC20 public immutable comp;
21 |
22 | constructor(
23 | IERC20 _comp,
24 | IVerifier _verifier,
25 | IHasher _hasher,
26 | uint256 _denomination,
27 | uint32 _merkleTreeHeight,
28 | IERC20 _token
29 | ) ERC20Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight, _token) {
30 | require(address(_comp) != address(0), "Invalid COMP token address");
31 | comp = _comp;
32 | }
33 |
34 | /// @dev Moves earned yield of the COMP token to the tornado governance contract
35 | /// To make it work you might need to call `comptroller.claimComp(cPoolAddress)` first
36 | function claimComp() external {
37 | comp.transfer(governance, comp.balanceOf(address(this)));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/deploy.js:
--------------------------------------------------------------------------------
1 | const eth = true
2 | const poolSize = '1000000000000000000'
3 | const hasherAddress = '0x83584f83f26aF4eDDA9CBe8C730bc87C364b28fe'
4 | const verifierAddress = '0xce172ce1F20EC0B3728c9965470eaf994A03557A'
5 | const deployerAddress = '0xCEe71753C9820f063b38FDbE4cFDAf1d3D928A80'
6 | const deploySalt = '0x0000000000000000000000000000000000000000000000000000000047941987'
7 | const rpcUrl = 'https://mainnet.infura.io'
8 |
9 | const Web3 = require('web3')
10 | const web3 = new Web3(rpcUrl)
11 |
12 | const contractData = require('./build/contracts/' + (eth ? 'ETHTornado.json' : 'ERC20Tornado.json'))
13 | const contract = new web3.eth.Contract(contractData.abi)
14 | const bytes = contract
15 | .deploy({
16 | data: contractData.bytecode,
17 | arguments: [verifierAddress, hasherAddress, poolSize, 20],
18 | })
19 | .encodeABI()
20 |
21 | console.log('Deploy bytecode', bytes)
22 |
23 | const deployer = new web3.eth.Contract(require('./build/contracts/IDeployer.json').abi, deployerAddress)
24 | const receipt = deployer.methods.deploy(bytes, deploySalt)
25 | receipt.then(console.log).catch(console.log)
26 |
--------------------------------------------------------------------------------
/docs/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tornadocash-community/tornado-core/1ef6a263ac6a0e476d063fcb269a9df65a1bd56a/docs/diagram.png
--------------------------------------------------------------------------------
/docs/enslookup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tornadocash-community/tornado-core/1ef6a263ac6a0e476d063fcb269a9df65a1bd56a/docs/enslookup.png
--------------------------------------------------------------------------------
/docs/resolver.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tornadocash-community/tornado-core/1ef6a263ac6a0e476d063fcb269a9df65a1bd56a/docs/resolver.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tornado test
6 |
7 |
8 |
9 | Open dev console!
10 | Make sure your Metamask is unlocked and connected to Kovan (or other network you've deployed your
11 | contract to)
12 | Deposit
13 | Withdraw
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/migrations/2_deploy_hasher.js:
--------------------------------------------------------------------------------
1 | /* global artifacts */
2 | const Hasher = artifacts.require('Hasher')
3 |
4 | module.exports = async function (deployer) {
5 | await deployer.deploy(Hasher)
6 | }
7 |
--------------------------------------------------------------------------------
/migrations/3_deploy_verifier.js:
--------------------------------------------------------------------------------
1 | /* global artifacts */
2 | const Verifier = artifacts.require('Verifier')
3 |
4 | module.exports = function (deployer) {
5 | deployer.deploy(Verifier)
6 | }
7 |
--------------------------------------------------------------------------------
/migrations/4_deploy_eth_tornado.js:
--------------------------------------------------------------------------------
1 | /* global artifacts */
2 | require('dotenv').config({ path: '../.env' })
3 | const ETHTornado = artifacts.require('ETHTornado')
4 | const Verifier = artifacts.require('Verifier')
5 | const Hasher = artifacts.require('Hasher')
6 |
7 | module.exports = function (deployer) {
8 | return deployer.then(async () => {
9 | const { MERKLE_TREE_HEIGHT, ETH_AMOUNT } = process.env
10 | const verifier = await Verifier.deployed()
11 | const hasher = await Hasher.deployed()
12 | const tornado = await deployer.deploy(
13 | ETHTornado,
14 | verifier.address,
15 | hasher.address,
16 | ETH_AMOUNT,
17 | MERKLE_TREE_HEIGHT,
18 | )
19 | console.log('ETHTornado address', tornado.address)
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/migrations/5_deploy_erc20_tornado.js:
--------------------------------------------------------------------------------
1 | /* global artifacts */
2 | require('dotenv').config({ path: '../.env' })
3 | const ERC20Tornado = artifacts.require('ERC20Tornado')
4 | const Verifier = artifacts.require('Verifier')
5 | const Hasher = artifacts.require('Hasher')
6 | const ERC20Mock = artifacts.require('ERC20Mock')
7 |
8 | module.exports = function (deployer) {
9 | return deployer.then(async () => {
10 | const { MERKLE_TREE_HEIGHT, ERC20_TOKEN, TOKEN_AMOUNT } = process.env
11 | const verifier = await Verifier.deployed()
12 | const hasher = await Hasher.deployed()
13 | let token = ERC20_TOKEN
14 | if (token === '') {
15 | const tokenInstance = await deployer.deploy(ERC20Mock)
16 | token = tokenInstance.address
17 | }
18 | const tornado = await deployer.deploy(
19 | ERC20Tornado,
20 | verifier.address,
21 | hasher.address,
22 | TOKEN_AMOUNT,
23 | MERKLE_TREE_HEIGHT,
24 | token,
25 | )
26 | console.log('ERC20Tornado address', tornado.address)
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "circuits",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build:circuit:compile": "npx circom circuits/withdraw.circom -o build/circuits/withdraw.json && npx snarkjs info -c build/circuits/withdraw.json",
8 | "build:circuit:setup": "npx snarkjs setup --protocol groth -c build/circuits/withdraw.json --pk build/circuits/withdraw_proving_key.json --vk build/circuits/withdraw_verification_key.json",
9 | "build:circuit:bin": "node node_modules/websnark/tools/buildpkey.js -i build/circuits/withdraw_proving_key.json -o build/circuits/withdraw_proving_key.bin",
10 | "build:circuit:contract": "npx snarkjs generateverifier -v build/circuits/Verifier.sol --vk build/circuits/withdraw_verification_key.json",
11 | "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",
12 | "build:contract": "npx truffle compile",
13 | "build:browserify": "npx browserify src/cli.js -o index.js --exclude worker_threads",
14 | "build": "npm run build:circuit && npm run build:contract && npm run build:browserify",
15 | "browserify": "npm run build:browserify",
16 | "test": "npx truffle test",
17 | "migrate": "npm run migrate:kovan",
18 | "migrate:dev": "npx truffle migrate --network development --reset",
19 | "migrate:kovan": "npx truffle migrate --network kovan --reset",
20 | "migrate:rinkeby": "npx truffle migrate --network rinkeby --reset",
21 | "migrate:mainnet": "npx truffle migrate --network mainnet",
22 | "eslint": "eslint --ext .js --ignore-path .gitignore .",
23 | "prettier:check": "prettier --check . --config .prettierrc",
24 | "prettier:fix": "prettier --write . --config .prettierrc",
25 | "lint": "yarn eslint && yarn prettier:check",
26 | "flat": "npx truffle-flattener contracts/ETHTornado.sol > ETHTornado_flat.sol && npx truffle-flattener contracts/ERC20Tornado.sol > ERC20Tornado_flat.sol",
27 | "download": "node scripts/downloadKeys.js",
28 | "coverage": "yarn truffle run coverage"
29 | },
30 | "keywords": [],
31 | "author": "",
32 | "license": "ISC",
33 | "dependencies": {
34 | "@openzeppelin/contracts": "^3.4.1",
35 | "@truffle/contract": "^4.0.39",
36 | "@truffle/hdwallet-provider": "^1.0.24",
37 | "axios": "^0.19.0",
38 | "babel-eslint": "^10.1.0",
39 | "bn-chai": "^1.0.1",
40 | "browserify": "^16.5.0",
41 | "chai": "^4.2.0",
42 | "chai-as-promised": "^7.1.1",
43 | "circom": "^0.0.35",
44 | "circomlib": "git+https://github.com/tornadocash/circomlib.git#c372f14d324d57339c88451834bf2824e73bbdbc",
45 | "commander": "^4.1.1",
46 | "dotenv": "^8.2.0",
47 | "eslint": "^7.19.0",
48 | "eslint-config-prettier": "^7.2.0",
49 | "eslint-plugin-prettier": "^3.3.1",
50 | "eth-json-rpc-filters": "^4.1.1",
51 | "fixed-merkle-tree": "^0.6.0",
52 | "ganache-cli": "^6.7.0",
53 | "prettier": "^2.2.1",
54 | "prettier-plugin-solidity": "^1.0.0-beta.3",
55 | "snarkjs": "git+https://github.com/tornadocash/snarkjs.git#869181cfaf7526fe8972073d31655493a04326d5",
56 | "solhint-plugin-prettier": "^0.0.5",
57 | "truffle": "^5.1.67",
58 | "truffle-flattener": "^1.4.2",
59 | "web3": "^1.3.4",
60 | "web3-utils": "^1.3.4",
61 | "websnark": "git+https://github.com/tornadocash/websnark.git#4c0af6a8b65aabea3c09f377f63c44e7a58afa6d",
62 | "solidity-coverage": "^0.7.20"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/scripts/compileHasher.js:
--------------------------------------------------------------------------------
1 | // Generates Hasher artifact at compile-time using Truffle's external compiler
2 | // mechanism
3 | const path = require('path')
4 | const fs = require('fs')
5 | const genContract = require('circomlib/src/mimcsponge_gencontract.js')
6 |
7 | // where Truffle will expect to find the results of the external compiler
8 | // command
9 | const outputPath = path.join(__dirname, '..', 'build', 'Hasher.json')
10 |
11 | function main() {
12 | const contract = {
13 | contractName: 'Hasher',
14 | abi: genContract.abi,
15 | bytecode: genContract.createCode('mimcsponge', 220),
16 | }
17 |
18 | fs.writeFileSync(outputPath, JSON.stringify(contract))
19 | }
20 |
21 | main()
22 |
--------------------------------------------------------------------------------
/scripts/downloadKeys.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const path = require('path')
3 | const fs = require('fs')
4 | const files = ['withdraw.json', 'withdraw_proving_key.bin', 'Verifier.sol', 'withdraw_verification_key.json']
5 | const circuitsPath = __dirname + '/../build/circuits'
6 | const contractsPath = __dirname + '/../build/contracts'
7 |
8 | async function downloadFile({ url, path }) {
9 | const writer = fs.createWriteStream(path)
10 |
11 | const response = await axios({
12 | url,
13 | method: 'GET',
14 | responseType: 'stream',
15 | })
16 |
17 | response.data.pipe(writer)
18 |
19 | return new Promise((resolve, reject) => {
20 | writer.on('finish', resolve)
21 | writer.on('error', reject)
22 | })
23 | }
24 |
25 | async function main() {
26 | const release = await axios.get('https://api.github.com/repos/tornadocash/tornado-core/releases/latest')
27 | const { assets } = release.data
28 | if (!fs.existsSync(circuitsPath)) {
29 | fs.mkdirSync(circuitsPath, { recursive: true })
30 | fs.mkdirSync(contractsPath, { recursive: true })
31 | }
32 | for (let asset of assets) {
33 | if (files.includes(asset.name)) {
34 | console.log(`Downloading ${asset.name} ...`)
35 | await downloadFile({
36 | url: asset.browser_download_url,
37 | path: path.resolve(__dirname, circuitsPath, asset.name),
38 | })
39 | }
40 | }
41 | }
42 |
43 | main()
44 |
--------------------------------------------------------------------------------
/scripts/ganacheHelper.js:
--------------------------------------------------------------------------------
1 | // This module is used only for tests
2 | function send(method, params = []) {
3 | return new Promise((resolve, reject) => {
4 | // eslint-disable-next-line no-undef
5 | web3.currentProvider.send({
6 | jsonrpc: '2.0',
7 | id: Date.now(),
8 | method,
9 | params,
10 | }, (err, res) => {
11 | return err ? reject(err) : resolve(res)
12 | })
13 | })
14 | }
15 |
16 | const takeSnapshot = async () => {
17 | return await send('evm_snapshot')
18 | }
19 |
20 | const traceTransaction = async (tx) => {
21 | return await send('debug_traceTransaction', [tx, {}])
22 | }
23 |
24 | const revertSnapshot = async (id) => {
25 | await send('evm_revert', [id])
26 | }
27 |
28 | const mineBlock = async (timestamp) => {
29 | await send('evm_mine', [timestamp])
30 | }
31 |
32 | const increaseTime = async (seconds) => {
33 | await send('evm_increaseTime', [seconds])
34 | }
35 |
36 | const minerStop = async () => {
37 | await send('miner_stop', [])
38 | }
39 |
40 | const minerStart = async () => {
41 | await send('miner_start', [])
42 | }
43 |
44 | module.exports = {
45 | takeSnapshot,
46 | revertSnapshot,
47 | mineBlock,
48 | minerStop,
49 | minerStart,
50 | increaseTime,
51 | traceTransaction,
52 | }
53 |
--------------------------------------------------------------------------------
/src/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Temporary demo client
3 | // Works both in browser and node.js
4 |
5 | require('dotenv').config()
6 | const fs = require('fs')
7 | const axios = require('axios')
8 | const assert = require('assert')
9 | const snarkjs = require('snarkjs')
10 | const crypto = require('crypto')
11 | const circomlib = require('circomlib')
12 | const bigInt = snarkjs.bigInt
13 | const merkleTree = require('fixed-merkle-tree')
14 | const Web3 = require('web3')
15 | const buildGroth16 = require('websnark/src/groth16')
16 | const websnarkUtils = require('websnark/src/utils')
17 | const { toWei, fromWei, toBN, BN } = require('web3-utils')
18 | const config = require('./config')
19 | const program = require('commander')
20 |
21 | let web3, tornado, circuit, proving_key, groth16, erc20, senderAccount, netId
22 | let MERKLE_TREE_HEIGHT, ETH_AMOUNT, TOKEN_AMOUNT, PRIVATE_KEY
23 |
24 | /** Whether we are in a browser or node.js */
25 | const inBrowser = (typeof window !== 'undefined')
26 | let isLocalRPC = false
27 |
28 | /** Generate random number of specified byte length */
29 | const rbigint = nbytes => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
30 |
31 | /** Compute pedersen hash */
32 | const pedersenHash = data => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
33 |
34 | /** BigNumber to hex string of specified length */
35 | function toHex(number, length = 32) {
36 | const str = number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)
37 | return '0x' + str.padStart(length * 2, '0')
38 | }
39 |
40 | /** Display ETH account balance */
41 | async function printETHBalance({ address, name }) {
42 | console.log(`${name} ETH balance is`, web3.utils.fromWei(await web3.eth.getBalance(address)))
43 | }
44 |
45 | /** Display ERC20 account balance */
46 | async function printERC20Balance({ address, name, tokenAddress }) {
47 | const erc20ContractJson = require(__dirname + '/../build/contracts/ERC20Mock.json')
48 | erc20 = tokenAddress ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : erc20
49 | console.log(`${name} Token Balance is`, web3.utils.fromWei(await erc20.methods.balanceOf(address).call()))
50 | }
51 |
52 | /**
53 | * Create deposit object from secret and nullifier
54 | */
55 | function createDeposit({ nullifier, secret }) {
56 | const deposit = { nullifier, secret }
57 | deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
58 | deposit.commitment = pedersenHash(deposit.preimage)
59 | deposit.commitmentHex = toHex(deposit.commitment)
60 | deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
61 | deposit.nullifierHex = toHex(deposit.nullifierHash)
62 | return deposit
63 | }
64 |
65 | /**
66 | * Make a deposit
67 | * @param currency Сurrency
68 | * @param amount Deposit amount
69 | */
70 | async function deposit({ currency, amount }) {
71 | const deposit = createDeposit({ nullifier: rbigint(31), secret: rbigint(31) })
72 | const note = toHex(deposit.preimage, 62)
73 | const noteString = `tornado-${currency}-${amount}-${netId}-${note}`
74 | console.log(`Your note: ${noteString}`)
75 | if (currency === 'eth') {
76 | await printETHBalance({ address: tornado._address, name: 'Tornado' })
77 | await printETHBalance({ address: senderAccount, name: 'Sender account' })
78 | const value = isLocalRPC ? ETH_AMOUNT : fromDecimals({ amount, decimals: 18 })
79 | console.log('Submitting deposit transaction')
80 | await tornado.methods.deposit(toHex(deposit.commitment)).send({ value, from: senderAccount, gas: 2e6 })
81 | await printETHBalance({ address: tornado._address, name: 'Tornado' })
82 | await printETHBalance({ address: senderAccount, name: 'Sender account' })
83 | } else { // a token
84 | await printERC20Balance({ address: tornado._address, name: 'Tornado' })
85 | await printERC20Balance({ address: senderAccount, name: 'Sender account' })
86 | const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
87 | const tokenAmount = isLocalRPC ? TOKEN_AMOUNT : fromDecimals({ amount, decimals })
88 | if (isLocalRPC) {
89 | console.log('Minting some test tokens to deposit')
90 | await erc20.methods.mint(senderAccount, tokenAmount).send({ from: senderAccount, gas: 2e6 })
91 | }
92 |
93 | const allowance = await erc20.methods.allowance(senderAccount, tornado._address).call({ from: senderAccount })
94 | console.log('Current allowance is', fromWei(allowance))
95 | if (toBN(allowance).lt(toBN(tokenAmount))) {
96 | console.log('Approving tokens for deposit')
97 | await erc20.methods.approve(tornado._address, tokenAmount).send({ from: senderAccount, gas: 1e6 })
98 | }
99 |
100 | console.log('Submitting deposit transaction')
101 | await tornado.methods.deposit(toHex(deposit.commitment)).send({ from: senderAccount, gas: 2e6 })
102 | await printERC20Balance({ address: tornado._address, name: 'Tornado' })
103 | await printERC20Balance({ address: senderAccount, name: 'Sender account' })
104 | }
105 |
106 | return noteString
107 | }
108 |
109 | /**
110 | * Generate merkle tree for a deposit.
111 | * Download deposit events from the tornado, reconstructs merkle tree, finds our deposit leaf
112 | * in it and generates merkle proof
113 | * @param deposit Deposit object
114 | */
115 | async function generateMerkleProof(deposit) {
116 | // Get all deposit events from smart contract and assemble merkle tree from them
117 | console.log('Getting current state from tornado contract')
118 | const events = await tornado.getPastEvents('Deposit', { fromBlock: 0, toBlock: 'latest' })
119 | const leaves = events
120 | .sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order
121 | .map(e => e.returnValues.commitment)
122 | const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves)
123 |
124 | // Find current commitment in the tree
125 | const depositEvent = events.find(e => e.returnValues.commitment === toHex(deposit.commitment))
126 | const leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
127 |
128 | // Validate that our data is correct
129 | const root = tree.root()
130 | const isValidRoot = await tornado.methods.isKnownRoot(toHex(root)).call()
131 | const isSpent = await tornado.methods.isSpent(toHex(deposit.nullifierHash)).call()
132 | assert(isValidRoot === true, 'Merkle tree is corrupted')
133 | assert(isSpent === false, 'The note is already spent')
134 | assert(leafIndex >= 0, 'The deposit is not found in the tree')
135 |
136 | // Compute merkle proof of our commitment
137 | const { pathElements, pathIndices } = tree.path(leafIndex)
138 | return { pathElements, pathIndices, root: tree.root() }
139 | }
140 |
141 | /**
142 | * Generate SNARK proof for withdrawal
143 | * @param deposit Deposit object
144 | * @param recipient Funds recipient
145 | * @param relayer Relayer address
146 | * @param fee Relayer fee
147 | * @param refund Receive ether for exchanged tokens
148 | */
149 | async function generateProof({ deposit, recipient, relayerAddress = 0, fee = 0, refund = 0 }) {
150 | // Compute merkle proof of our commitment
151 | const { root, pathElements, pathIndices } = await generateMerkleProof(deposit)
152 |
153 | // Prepare circuit input
154 | const input = {
155 | // Public snark inputs
156 | root: root,
157 | nullifierHash: deposit.nullifierHash,
158 | recipient: bigInt(recipient),
159 | relayer: bigInt(relayerAddress),
160 | fee: bigInt(fee),
161 | refund: bigInt(refund),
162 |
163 | // Private snark inputs
164 | nullifier: deposit.nullifier,
165 | secret: deposit.secret,
166 | pathElements: pathElements,
167 | pathIndices: pathIndices,
168 | }
169 |
170 | console.log('Generating SNARK proof')
171 | console.time('Proof time')
172 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
173 | const { proof } = websnarkUtils.toSolidityInput(proofData)
174 | console.timeEnd('Proof time')
175 |
176 | const args = [
177 | toHex(input.root),
178 | toHex(input.nullifierHash),
179 | toHex(input.recipient, 20),
180 | toHex(input.relayer, 20),
181 | toHex(input.fee),
182 | toHex(input.refund),
183 | ]
184 |
185 | return { proof, args }
186 | }
187 |
188 | /**
189 | * Do an ETH withdrawal
190 | * @param noteString Note to withdraw
191 | * @param recipient Recipient address
192 | */
193 | async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund = '0' }) {
194 | if (currency === 'eth' && refund !== '0') {
195 | throw new Error('The ETH purchase is supposted to be 0 for ETH withdrawals')
196 | }
197 | refund = toWei(refund)
198 | if (relayerURL) {
199 | if (relayerURL.endsWith('.eth')) {
200 | throw new Error('ENS name resolving is not supported. Please provide DNS name of the relayer. See instuctions in README.md')
201 | }
202 | const relayerStatus = await axios.get(relayerURL + '/status')
203 | const { relayerAddress, netId, gasPrices, ethPrices, relayerServiceFee } = relayerStatus.data
204 | assert(netId === await web3.eth.net.getId() || netId === '*', 'This relay is for different network')
205 | console.log('Relay address: ', relayerAddress)
206 |
207 | const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
208 | const fee = calculateFee({ gasPrices, currency, amount, refund, ethPrices, relayerServiceFee, decimals })
209 | if (fee.gt(fromDecimals({ amount, decimals }))) {
210 | throw new Error('Too high refund')
211 | }
212 | const { proof, args } = await generateProof({ deposit, recipient, relayerAddress, fee, refund })
213 |
214 | console.log('Sending withdraw transaction through relay')
215 | try {
216 | const relay = await axios.post(relayerURL + '/relay', { contract: tornado._address, proof, args })
217 | if (netId === 1 || netId === 42) {
218 | console.log(`Transaction submitted through the relay. View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${relay.data.txHash}`)
219 | } else {
220 | console.log(`Transaction submitted through the relay. The transaction hash is ${relay.data.txHash}`)
221 | }
222 |
223 | const receipt = await waitForTxReceipt({ txHash: relay.data.txHash })
224 | console.log('Transaction mined in block', receipt.blockNumber)
225 | } catch (e) {
226 | if (e.response) {
227 | console.error(e.response.data.error)
228 | } else {
229 | console.error(e.message)
230 | }
231 | }
232 | } else { // using private key
233 | const { proof, args } = await generateProof({ deposit, recipient, refund })
234 |
235 | console.log('Submitting withdraw transaction')
236 | await tornado.methods.withdraw(proof, ...args).send({ from: senderAccount, value: refund.toString(), gas: 1e6 })
237 | .on('transactionHash', function (txHash) {
238 | if (netId === 1 || netId === 42) {
239 | console.log(`View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${txHash}`)
240 | } else {
241 | console.log(`The transaction hash is ${txHash}`)
242 | }
243 | }).on('error', function (e) {
244 | console.error('on transactionHash error', e.message)
245 | })
246 | }
247 | console.log('Done')
248 | }
249 |
250 | function fromDecimals({ amount, decimals }) {
251 | amount = amount.toString()
252 | let ether = amount.toString()
253 | const base = new BN('10').pow(new BN(decimals))
254 | const baseLength = base.toString(10).length - 1 || 1
255 |
256 | const negative = ether.substring(0, 1) === '-'
257 | if (negative) {
258 | ether = ether.substring(1)
259 | }
260 |
261 | if (ether === '.') {
262 | throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, invalid value')
263 | }
264 |
265 | // Split it into a whole and fractional part
266 | const comps = ether.split('.')
267 | if (comps.length > 2) {
268 | throw new Error(
269 | '[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal points',
270 | )
271 | }
272 |
273 | let whole = comps[0]
274 | let fraction = comps[1]
275 |
276 | if (!whole) {
277 | whole = '0'
278 | }
279 | if (!fraction) {
280 | fraction = '0'
281 | }
282 | if (fraction.length > baseLength) {
283 | throw new Error(
284 | '[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal places',
285 | )
286 | }
287 |
288 | while (fraction.length < baseLength) {
289 | fraction += '0'
290 | }
291 |
292 | whole = new BN(whole)
293 | fraction = new BN(fraction)
294 | let wei = whole.mul(base).add(fraction)
295 |
296 | if (negative) {
297 | wei = wei.mul(negative)
298 | }
299 |
300 | return new BN(wei.toString(10), 10)
301 | }
302 |
303 | function toDecimals(value, decimals, fixed) {
304 | const zero = new BN(0)
305 | const negative1 = new BN(-1)
306 | decimals = decimals || 18
307 | fixed = fixed || 7
308 |
309 | value = new BN(value)
310 | const negative = value.lt(zero)
311 | const base = new BN('10').pow(new BN(decimals))
312 | const baseLength = base.toString(10).length - 1 || 1
313 |
314 | if (negative) {
315 | value = value.mul(negative1)
316 | }
317 |
318 | let fraction = value.mod(base).toString(10)
319 | while (fraction.length < baseLength) {
320 | fraction = `0${fraction}`
321 | }
322 | fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1]
323 |
324 | const whole = value.div(base).toString(10)
325 | value = `${whole}${fraction === '0' ? '' : `.${fraction}`}`
326 |
327 | if (negative) {
328 | value = `-${value}`
329 | }
330 |
331 | if (fixed) {
332 | value = value.slice(0, fixed)
333 | }
334 |
335 | return value
336 | }
337 |
338 | function getCurrentNetworkName() {
339 | switch (netId) {
340 | case 1:
341 | return ''
342 | case 42:
343 | return 'kovan.'
344 | }
345 |
346 | }
347 |
348 | function calculateFee({ gasPrices, currency, amount, refund, ethPrices, relayerServiceFee, decimals }) {
349 | const decimalsPoint = Math.floor(relayerServiceFee) === Number(relayerServiceFee) ?
350 | 0 :
351 | relayerServiceFee.toString().split('.')[1].length
352 | const roundDecimal = 10 ** decimalsPoint
353 | const total = toBN(fromDecimals({ amount, decimals }))
354 | const feePercent = total.mul(toBN(relayerServiceFee * roundDecimal)).div(toBN(roundDecimal * 100))
355 | const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(5e5))
356 | let desiredFee
357 | switch (currency) {
358 | case 'eth': {
359 | desiredFee = expense.add(feePercent)
360 | break
361 | }
362 | default: {
363 | desiredFee = expense.add(toBN(refund))
364 | .mul(toBN(10 ** decimals))
365 | .div(toBN(ethPrices[currency]))
366 | desiredFee = desiredFee.add(feePercent)
367 | break
368 | }
369 | }
370 | return desiredFee
371 | }
372 |
373 | /**
374 | * Waits for transaction to be mined
375 | * @param txHash Hash of transaction
376 | * @param attempts
377 | * @param delay
378 | */
379 | function waitForTxReceipt({ txHash, attempts = 60, delay = 1000 }) {
380 | return new Promise((resolve, reject) => {
381 | const checkForTx = async (txHash, retryAttempt = 0) => {
382 | const result = await web3.eth.getTransactionReceipt(txHash)
383 | if (!result || !result.blockNumber) {
384 | if (retryAttempt <= attempts) {
385 | setTimeout(() => checkForTx(txHash, retryAttempt + 1), delay)
386 | } else {
387 | reject(new Error('tx was not mined'))
388 | }
389 | } else {
390 | resolve(result)
391 | }
392 | }
393 | checkForTx(txHash)
394 | })
395 | }
396 |
397 | /**
398 | * Parses Tornado.cash note
399 | * @param noteString the note
400 | */
401 | function parseNote(noteString) {
402 | const noteRegex = /tornado-(?\w+)-(?[\d.]+)-(?\d+)-0x(?[0-9a-fA-F]{124})/g
403 | const match = noteRegex.exec(noteString)
404 | if (!match) {
405 | throw new Error('The note has invalid format')
406 | }
407 |
408 | const buf = Buffer.from(match.groups.note, 'hex')
409 | const nullifier = bigInt.leBuff2int(buf.slice(0, 31))
410 | const secret = bigInt.leBuff2int(buf.slice(31, 62))
411 | const deposit = createDeposit({ nullifier, secret })
412 | const netId = Number(match.groups.netId)
413 |
414 | return { currency: match.groups.currency, amount: match.groups.amount, netId, deposit }
415 | }
416 |
417 | async function loadDepositData({ deposit }) {
418 | try {
419 | const eventWhenHappened = await tornado.getPastEvents('Deposit', {
420 | filter: {
421 | commitment: deposit.commitmentHex,
422 | },
423 | fromBlock: 0,
424 | toBlock: 'latest',
425 | })
426 | if (eventWhenHappened.length === 0) {
427 | throw new Error('There is no related deposit, the note is invalid')
428 | }
429 |
430 | const { timestamp } = eventWhenHappened[0].returnValues
431 | const txHash = eventWhenHappened[0].transactionHash
432 | const isSpent = await tornado.methods.isSpent(deposit.nullifierHex).call()
433 | const receipt = await web3.eth.getTransactionReceipt(txHash)
434 |
435 | return { timestamp, txHash, isSpent, from: receipt.from, commitment: deposit.commitmentHex }
436 | } catch (e) {
437 | console.error('loadDepositData', e)
438 | }
439 | return {}
440 | }
441 | async function loadWithdrawalData({ amount, currency, deposit }) {
442 | try {
443 | const events = await tornado.getPastEvents('Withdrawal', {
444 | fromBlock: 0,
445 | toBlock: 'latest',
446 | })
447 |
448 | const withdrawEvent = events.filter((event) => {
449 | return event.returnValues.nullifierHash === deposit.nullifierHex
450 | })[0]
451 |
452 | const fee = withdrawEvent.returnValues.fee
453 | const decimals = config.deployments[`netId${netId}`][currency].decimals
454 | const withdrawalAmount = toBN(fromDecimals({ amount, decimals })).sub(
455 | toBN(fee),
456 | )
457 | const { timestamp } = await web3.eth.getBlock(withdrawEvent.blockHash)
458 | return {
459 | amount: toDecimals(withdrawalAmount, decimals, 9),
460 | txHash: withdrawEvent.transactionHash,
461 | to: withdrawEvent.returnValues.to,
462 | timestamp,
463 | nullifier: deposit.nullifierHex,
464 | fee: toDecimals(fee, decimals, 9),
465 | }
466 | } catch (e) {
467 | console.error('loadWithdrawalData', e)
468 | }
469 | }
470 |
471 | /**
472 | * Init web3, contracts, and snark
473 | */
474 | async function init({ rpc, noteNetId, currency = 'dai', amount = '100' }) {
475 | let contractJson, erc20ContractJson, erc20tornadoJson, tornadoAddress, tokenAddress
476 | // TODO do we need this? should it work in browser really?
477 | if (inBrowser) {
478 | // Initialize using injected web3 (Metamask)
479 | // To assemble web version run `npm run browserify`
480 | web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 })
481 | contractJson = await (await fetch('build/contracts/ETHTornado.json')).json()
482 | circuit = await (await fetch('build/circuits/withdraw.json')).json()
483 | proving_key = await (await fetch('build/circuits/withdraw_proving_key.bin')).arrayBuffer()
484 | MERKLE_TREE_HEIGHT = 20
485 | ETH_AMOUNT = 1e18
486 | TOKEN_AMOUNT = 1e19
487 | senderAccount = (await web3.eth.getAccounts())[0]
488 | } else {
489 | // Initialize from local node
490 | web3 = new Web3(rpc, null, { transactionConfirmationBlocks: 1 })
491 | contractJson = require(__dirname + '/../build/contracts/ETHTornado.json')
492 | circuit = require(__dirname + '/../build/circuits/withdraw.json')
493 | proving_key = fs.readFileSync(__dirname + '/../build/circuits/withdraw_proving_key.bin').buffer
494 | MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT || 20
495 | ETH_AMOUNT = process.env.ETH_AMOUNT
496 | TOKEN_AMOUNT = process.env.TOKEN_AMOUNT
497 | PRIVATE_KEY = process.env.PRIVATE_KEY
498 | if (PRIVATE_KEY) {
499 | const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY)
500 | web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY)
501 | web3.eth.defaultAccount = account.address
502 | senderAccount = account.address
503 | } else {
504 | console.log('Warning! PRIVATE_KEY not found. Please provide PRIVATE_KEY in .env file if you deposit')
505 | }
506 | erc20ContractJson = require(__dirname + '/../build/contracts/ERC20Mock.json')
507 | erc20tornadoJson = require(__dirname + '/../build/contracts/ERC20Tornado.json')
508 | }
509 | // groth16 initialises a lot of Promises that will never be resolved, that's why we need to use process.exit to terminate the CLI
510 | groth16 = await buildGroth16()
511 | netId = await web3.eth.net.getId()
512 | if (noteNetId && Number(noteNetId) !== netId) {
513 | throw new Error('This note is for a different network. Specify the --rpc option explicitly')
514 | }
515 | isLocalRPC = netId > 42
516 |
517 | if (isLocalRPC) {
518 | tornadoAddress = currency === 'eth' ? contractJson.networks[netId].address : erc20tornadoJson.networks[netId].address
519 | tokenAddress = currency !== 'eth' ? erc20ContractJson.networks[netId].address : null
520 | senderAccount = (await web3.eth.getAccounts())[0]
521 | } else {
522 | try {
523 | tornadoAddress = config.deployments[`netId${netId}`][currency].instanceAddress[amount]
524 | if (!tornadoAddress) {
525 | throw new Error()
526 | }
527 | tokenAddress = config.deployments[`netId${netId}`][currency].tokenAddress
528 | } catch (e) {
529 | console.error('There is no such tornado instance, check the currency and amount you provide')
530 | process.exit(1)
531 | }
532 | }
533 | tornado = new web3.eth.Contract(contractJson.abi, tornadoAddress)
534 | erc20 = currency !== 'eth' ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : {}
535 | }
536 |
537 | async function main() {
538 | if (inBrowser) {
539 | const instance = { currency: 'eth', amount: '0.1' }
540 | await init(instance)
541 | window.deposit = async () => {
542 | await deposit(instance)
543 | }
544 | window.withdraw = async () => {
545 | const noteString = prompt('Enter the note to withdraw')
546 | const recipient = (await web3.eth.getAccounts())[0]
547 |
548 | const { currency, amount, netId, deposit } = parseNote(noteString)
549 | await init({ noteNetId: netId, currency, amount })
550 | await withdraw({ deposit, currency, amount, recipient })
551 | }
552 | } else {
553 | program
554 | .option('-r, --rpc ', 'The RPC, CLI should interact with', 'http://localhost:8545')
555 | .option('-R, --relayer ', 'Withdraw via relayer')
556 | program
557 | .command('deposit ')
558 | .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.')
559 | .action(async (currency, amount) => {
560 | currency = currency.toLowerCase()
561 | await init({ rpc: program.rpc, currency, amount })
562 | await deposit({ currency, amount })
563 | })
564 | program
565 | .command('withdraw [ETH_purchase]')
566 | .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.')
567 | .action(async (noteString, recipient, refund) => {
568 | const { currency, amount, netId, deposit } = parseNote(noteString)
569 | await init({ rpc: program.rpc, noteNetId: netId, currency, amount })
570 | await withdraw({ deposit, currency, amount, recipient, refund, relayerURL: program.relayer })
571 | })
572 | program
573 | .command('balance [token_address]')
574 | .description('Check ETH and ERC20 balance')
575 | .action(async (address, tokenAddress) => {
576 | await init({ rpc: program.rpc })
577 | await printETHBalance({ address, name: '' })
578 | if (tokenAddress) {
579 | await printERC20Balance({ address, name: '', tokenAddress })
580 | }
581 | })
582 | program
583 | .command('compliance ')
584 | .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.')
585 | .action(async (noteString) => {
586 | const { currency, amount, netId, deposit } = parseNote(noteString)
587 | await init({ rpc: program.rpc, noteNetId: netId, currency, amount })
588 | const depositInfo = await loadDepositData({ deposit })
589 | const depositDate = new Date(depositInfo.timestamp * 1000)
590 | console.log('\n=============Deposit=================')
591 | console.log('Deposit :', amount, currency)
592 | console.log('Date :', depositDate.toLocaleDateString(), depositDate.toLocaleTimeString())
593 | console.log('From :', `https://${getCurrentNetworkName()}etherscan.io/address/${depositInfo.from}`)
594 | console.log('Transaction :', `https://${getCurrentNetworkName()}etherscan.io/tx/${depositInfo.txHash}`)
595 | console.log('Commitment :', depositInfo.commitment)
596 | if (deposit.isSpent) {
597 | console.log('The note was not spent')
598 | }
599 |
600 | const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit })
601 | const withdrawalDate = new Date(withdrawInfo.timestamp * 1000)
602 | console.log('\n=============Withdrawal==============')
603 | console.log('Withdrawal :', withdrawInfo.amount, currency)
604 | console.log('Relayer Fee :', withdrawInfo.fee, currency)
605 | console.log('Date :', withdrawalDate.toLocaleDateString(), withdrawalDate.toLocaleTimeString())
606 | console.log('To :', `https://${getCurrentNetworkName()}etherscan.io/address/${withdrawInfo.to}`)
607 | console.log('Transaction :', `https://${getCurrentNetworkName()}etherscan.io/tx/${withdrawInfo.txHash}`)
608 | console.log('Nullifier :', withdrawInfo.nullifier)
609 | })
610 | program
611 | .command('test')
612 | .description('Perform an automated test. It deposits and withdraws one ETH and one ERC20 note. Uses ganache.')
613 | .action(async () => {
614 | console.log('Start performing ETH deposit-withdraw test')
615 | let currency = 'eth'
616 | let amount = '0.1'
617 | await init({ rpc: program.rpc, currency, amount })
618 | let noteString = await deposit({ currency, amount })
619 | let parsedNote = parseNote(noteString)
620 | await withdraw({ deposit: parsedNote.deposit, currency, amount, recipient: senderAccount, relayerURL: program.relayer })
621 |
622 | console.log('\nStart performing DAI deposit-withdraw test')
623 | currency = 'dai'
624 | amount = '100'
625 | await init({ rpc: program.rpc, currency, amount })
626 | noteString = await deposit({ currency, amount })
627 | ; (parsedNote = parseNote(noteString))
628 | await withdraw({ deposit: parsedNote.deposit, currency, amount, recipient: senderAccount, refund: '0.02', relayerURL: program.relayer })
629 | })
630 | try {
631 | await program.parseAsync(process.argv)
632 | process.exit(0)
633 | } catch (e) {
634 | console.log('Error:', e)
635 | process.exit(1)
636 | }
637 | }
638 | }
639 |
640 | main()
641 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 |
3 | module.exports = {
4 | deployments: {
5 | netId1: {
6 | eth: {
7 | instanceAddress: {
8 | 0.1: '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
9 | 1: '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
10 | 10: '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
11 | 100: '0xA160cdAB225685dA1d56aa342Ad8841c3b53f291',
12 | },
13 | symbol: 'ETH',
14 | decimals: 18,
15 | },
16 | dai: {
17 | instanceAddress: {
18 | 100: '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
19 | 1000: '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
20 | 10000: '0xF60dD140cFf0706bAE9Cd734Ac3ae76AD9eBC32A',
21 | 100000: undefined,
22 | },
23 | tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
24 | symbol: 'DAI',
25 | decimals: 18,
26 | },
27 | cdai: {
28 | instanceAddress: {
29 | 5000: '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
30 | 50000: '0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659',
31 | 500000: '0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00',
32 | 5000000: undefined,
33 | },
34 | tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643',
35 | symbol: 'cDAI',
36 | decimals: 8,
37 | },
38 | usdc: {
39 | instanceAddress: {
40 | 100: '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
41 | 1000: '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
42 | 10000: '0xD691F27f38B395864Ea86CfC7253969B409c362d',
43 | 100000: undefined,
44 | },
45 | tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
46 | symbol: 'USDC',
47 | decimals: 6,
48 | },
49 | cusdc: {
50 | instanceAddress: {
51 | 5000: '0xaEaaC358560e11f52454D997AAFF2c5731B6f8a6',
52 | 50000: '0x1356c899D8C9467C7f71C195612F8A395aBf2f0a',
53 | 500000: '0xA60C772958a3eD56c1F15dD055bA37AC8e523a0D',
54 | 5000000: undefined,
55 | },
56 | tokenAddress: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
57 | symbol: 'cUSDC',
58 | decimals: 8,
59 | },
60 | usdt: {
61 | instanceAddress: {
62 | 100: '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
63 | 1000: '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
64 | 10000: '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a',
65 | 100000: '0x9AD122c22B14202B4490eDAf288FDb3C7cb3ff5E',
66 | },
67 | tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
68 | symbol: 'USDT',
69 | decimals: 6,
70 | },
71 | },
72 | netId42: {
73 | eth: {
74 | instanceAddress: {
75 | 0.1: '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
76 | 1: '0xD6a6AC46d02253c938B96D12BE439F570227aE8E',
77 | 10: '0xe1BE96331391E519471100c3c1528B66B8F4e5a7',
78 | 100: '0xd037E0Ac98Dab2fCb7E296c69C6e52767Ae5414D',
79 | },
80 | symbol: 'ETH',
81 | decimals: 18,
82 | },
83 | dai: {
84 | instanceAddress: {
85 | 100: '0xdf2d3cC5F361CF95b3f62c4bB66deFe3FDE47e3D',
86 | 1000: '0xD96291dFa35d180a71964D0894a1Ae54247C4ccD',
87 | 10000: '0xb192794f72EA45e33C3DF6fe212B9c18f6F45AE3',
88 | 100000: undefined,
89 | },
90 | tokenAddress: '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa',
91 | symbol: 'DAI',
92 | decimals: 18,
93 | },
94 | cdai: {
95 | instanceAddress: {
96 | 5000: '0x6Fc9386ABAf83147b3a89C36D422c625F44121C8',
97 | 50000: '0x7182EA067e0f050997444FCb065985Fd677C16b6',
98 | 500000: '0xC22ceFd90fbd1FdEeE554AE6Cc671179BC3b10Ae',
99 | 5000000: undefined,
100 | },
101 | tokenAddress: '0xe7bc397DBd069fC7d0109C0636d06888bb50668c',
102 | symbol: 'cDAI',
103 | decimals: 8,
104 | },
105 | usdc: {
106 | instanceAddress: {
107 | 100: '0x137E2B6d185018e7f09f6cf175a970e7fC73826C',
108 | 1000: '0xcC7f1633A5068E86E3830e692e3e3f8f520525Af',
109 | 10000: '0x28C8f149a0ab8A9bdB006B8F984fFFCCE52ef5EF',
110 | 100000: undefined,
111 | },
112 | tokenAddress: '0x75B0622Cec14130172EaE9Cf166B92E5C112FaFF',
113 | symbol: 'USDC',
114 | decimals: 6,
115 | },
116 | cusdc: {
117 | instanceAddress: {
118 | 5000: '0xc0648F28ABA385c8a1421Bbf1B59e3c474F89cB0',
119 | 50000: '0x0C53853379c6b1A7B74E0A324AcbDD5Eabd4981D',
120 | 500000: '0xf84016A0E03917cBe700D318EB1b7a53e6e3dEe1',
121 | 5000000: undefined,
122 | },
123 | tokenAddress: '0xcfC9bB230F00bFFDB560fCe2428b4E05F3442E35',
124 | symbol: 'cUSDC',
125 | decimals: 8,
126 | },
127 | usdt: {
128 | instanceAddress: {
129 | 100: '0x327853Da7916a6A0935563FB1919A48843036b42',
130 | 1000: '0x531AA4DF5858EA1d0031Dad16e3274609DE5AcC0',
131 | 10000: '0x0958275F0362cf6f07D21373aEE0cf37dFe415dD',
132 | 100000: '0x14aEd24B67EaF3FF28503eB92aeb217C47514364',
133 | },
134 | tokenAddress: '0x03c5F29e9296006876d8DF210BCFfD7EA5Db1Cf1',
135 | symbol: 'USDT',
136 | decimals: 6,
137 | },
138 | },
139 | },
140 | }
141 |
--------------------------------------------------------------------------------
/src/minimal-demo.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const assert = require('assert')
3 | const { bigInt } = require('snarkjs')
4 | const crypto = require('crypto')
5 | const circomlib = require('circomlib')
6 | const merkleTree = require('fixed-merkle-tree')
7 | const Web3 = require('web3')
8 | const buildGroth16 = require('websnark/src/groth16')
9 | const websnarkUtils = require('websnark/src/utils')
10 | const { toWei } = require('web3-utils')
11 |
12 | let web3, contract, netId, circuit, proving_key, groth16
13 | const MERKLE_TREE_HEIGHT = 20
14 | const RPC_URL = 'https://kovan.infura.io/v3/0279e3bdf3ee49d0b547c643c2ef78ef'
15 | const PRIVATE_KEY = 'ad5b6eb7ee88173fa43dedcff8b1d9024d03f6307a1143ecf04bea8ed40f283f' // 0x94462e71A887756704f0fb1c0905264d487972fE
16 | const CONTRACT_ADDRESS = '0xD6a6AC46d02253c938B96D12BE439F570227aE8E'
17 | const AMOUNT = '1'
18 | // CURRENCY = 'ETH'
19 |
20 | /** Generate random number of specified byte length */
21 | const rbigint = (nbytes) => bigInt.leBuff2int(crypto.randomBytes(nbytes))
22 |
23 | /** Compute pedersen hash */
24 | const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
25 |
26 | /** BigNumber to hex string of specified length */
27 | const toHex = (number, length = 32) =>
28 | '0x' +
29 | (number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)).padStart(length * 2, '0')
30 |
31 | /**
32 | * Create deposit object from secret and nullifier
33 | */
34 | function createDeposit(nullifier, secret) {
35 | let deposit = { nullifier, secret }
36 | deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
37 | deposit.commitment = pedersenHash(deposit.preimage)
38 | deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
39 | return deposit
40 | }
41 |
42 | /**
43 | * Make an ETH deposit
44 | */
45 | async function deposit() {
46 | const deposit = createDeposit(rbigint(31), rbigint(31))
47 | console.log('Sending deposit transaction...')
48 | const tx = await contract.methods
49 | .deposit(toHex(deposit.commitment))
50 | .send({ value: toWei(AMOUNT), from: web3.eth.defaultAccount, gas: 2e6 })
51 | console.log(`https://kovan.etherscan.io/tx/${tx.transactionHash}`)
52 | return `tornado-eth-${AMOUNT}-${netId}-${toHex(deposit.preimage, 62)}`
53 | }
54 |
55 | /**
56 | * Do an ETH withdrawal
57 | * @param note Note to withdraw
58 | * @param recipient Recipient address
59 | */
60 | async function withdraw(note, recipient) {
61 | const deposit = parseNote(note)
62 | const { proof, args } = await generateSnarkProof(deposit, recipient)
63 | console.log('Sending withdrawal transaction...')
64 | const tx = await contract.methods.withdraw(proof, ...args).send({ from: web3.eth.defaultAccount, gas: 1e6 })
65 | console.log(`https://kovan.etherscan.io/tx/${tx.transactionHash}`)
66 | }
67 |
68 | /**
69 | * Parses Tornado.cash note
70 | * @param noteString the note
71 | */
72 | function parseNote(noteString) {
73 | const noteRegex = /tornado-(?\w+)-(?[\d.]+)-(?\d+)-0x(?[0-9a-fA-F]{124})/g
74 | const match = noteRegex.exec(noteString)
75 |
76 | // we are ignoring `currency`, `amount`, and `netId` for this minimal example
77 | const buf = Buffer.from(match.groups.note, 'hex')
78 | const nullifier = bigInt.leBuff2int(buf.slice(0, 31))
79 | const secret = bigInt.leBuff2int(buf.slice(31, 62))
80 | return createDeposit(nullifier, secret)
81 | }
82 |
83 | /**
84 | * Generate merkle tree for a deposit.
85 | * Download deposit events from the contract, reconstructs merkle tree, finds our deposit leaf
86 | * in it and generates merkle proof
87 | * @param deposit Deposit object
88 | */
89 | async function generateMerkleProof(deposit) {
90 | console.log('Getting contract state...')
91 | const events = await contract.getPastEvents('Deposit', { fromBlock: 0, toBlock: 'latest' })
92 | const leaves = events
93 | .sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order
94 | .map((e) => e.returnValues.commitment)
95 | const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves)
96 |
97 | // Find current commitment in the tree
98 | let depositEvent = events.find((e) => e.returnValues.commitment === toHex(deposit.commitment))
99 | let leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
100 |
101 | // Validate that our data is correct (optional)
102 | const isValidRoot = await contract.methods.isKnownRoot(toHex(tree.root())).call()
103 | const isSpent = await contract.methods.isSpent(toHex(deposit.nullifierHash)).call()
104 | assert(isValidRoot === true, 'Merkle tree is corrupted')
105 | assert(isSpent === false, 'The note is already spent')
106 | assert(leafIndex >= 0, 'The deposit is not found in the tree')
107 |
108 | // Compute merkle proof of our commitment
109 | const { pathElements, pathIndices } = tree.path(leafIndex)
110 | return { pathElements, pathIndices, root: tree.root() }
111 | }
112 |
113 | /**
114 | * Generate SNARK proof for withdrawal
115 | * @param deposit Deposit object
116 | * @param recipient Funds recipient
117 | */
118 | async function generateSnarkProof(deposit, recipient) {
119 | // Compute merkle proof of our commitment
120 | const { root, pathElements, pathIndices } = await generateMerkleProof(deposit)
121 |
122 | // Prepare circuit input
123 | const input = {
124 | // Public snark inputs
125 | root: root,
126 | nullifierHash: deposit.nullifierHash,
127 | recipient: bigInt(recipient),
128 | relayer: 0,
129 | fee: 0,
130 | refund: 0,
131 |
132 | // Private snark inputs
133 | nullifier: deposit.nullifier,
134 | secret: deposit.secret,
135 | pathElements: pathElements,
136 | pathIndices: pathIndices,
137 | }
138 |
139 | console.log('Generating SNARK proof...')
140 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
141 | const { proof } = websnarkUtils.toSolidityInput(proofData)
142 |
143 | const args = [
144 | toHex(input.root),
145 | toHex(input.nullifierHash),
146 | toHex(input.recipient, 20),
147 | toHex(input.relayer, 20),
148 | toHex(input.fee),
149 | toHex(input.refund),
150 | ]
151 |
152 | return { proof, args }
153 | }
154 |
155 | async function main() {
156 | web3 = new Web3(new Web3.providers.HttpProvider(RPC_URL, { timeout: 5 * 60 * 1000 }), null, {
157 | transactionConfirmationBlocks: 1,
158 | })
159 | circuit = require(__dirname + '/../build/circuits/withdraw.json')
160 | proving_key = fs.readFileSync(__dirname + '/../build/circuits/withdraw_proving_key.bin').buffer
161 | groth16 = await buildGroth16()
162 | netId = await web3.eth.net.getId()
163 | contract = new web3.eth.Contract(require('../build/contracts/ETHTornado.json').abi, CONTRACT_ADDRESS)
164 | const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY)
165 | web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY)
166 | // eslint-disable-next-line require-atomic-updates
167 | web3.eth.defaultAccount = account.address
168 |
169 | const note = await deposit()
170 | console.log('Deposited note:', note)
171 | await withdraw(note, web3.eth.defaultAccount)
172 | console.log('Done')
173 | process.exit()
174 | }
175 |
176 | main()
177 |
--------------------------------------------------------------------------------
/test/ERC20Tornado.test.js:
--------------------------------------------------------------------------------
1 | /* global artifacts, web3, contract */
2 | require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
3 | const fs = require('fs')
4 |
5 | const { toBN } = require('web3-utils')
6 | const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
7 |
8 | const Tornado = artifacts.require('./ERC20Tornado.sol')
9 | const BadRecipient = artifacts.require('./BadRecipient.sol')
10 | const Token = artifacts.require('./ERC20Mock.sol')
11 | const USDTToken = artifacts.require('./IUSDT.sol')
12 | const { ETH_AMOUNT, TOKEN_AMOUNT, MERKLE_TREE_HEIGHT, ERC20_TOKEN } = process.env
13 |
14 | const websnarkUtils = require('websnark/src/utils')
15 | const buildGroth16 = require('websnark/src/groth16')
16 | const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts
17 | const snarkjs = require('snarkjs')
18 | const bigInt = snarkjs.bigInt
19 | const crypto = require('crypto')
20 | const circomlib = require('circomlib')
21 | const MerkleTree = require('fixed-merkle-tree')
22 |
23 | const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
24 | const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
25 | const toFixedHex = (number, length = 32) =>
26 | '0x' +
27 | bigInt(number)
28 | .toString(16)
29 | .padStart(length * 2, '0')
30 | const getRandomRecipient = () => rbigint(20)
31 |
32 | function generateDeposit() {
33 | let deposit = {
34 | secret: rbigint(31),
35 | nullifier: rbigint(31),
36 | }
37 | const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
38 | deposit.commitment = pedersenHash(preimage)
39 | return deposit
40 | }
41 |
42 | contract('ERC20Tornado', (accounts) => {
43 | let tornado
44 | let token
45 | let usdtToken
46 | let badRecipient
47 | const sender = accounts[0]
48 | const operator = accounts[0]
49 | const levels = MERKLE_TREE_HEIGHT || 16
50 | let tokenDenomination = TOKEN_AMOUNT || '1000000000000000000' // 1 ether
51 | let snapshotId
52 | let tree
53 | const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
54 | const refund = ETH_AMOUNT || '1000000000000000000' // 1 ether
55 | let recipient = getRandomRecipient()
56 | const relayer = accounts[1]
57 | let groth16
58 | let circuit
59 | let proving_key
60 |
61 | before(async () => {
62 | tree = new MerkleTree(levels)
63 | tornado = await Tornado.deployed()
64 | if (ERC20_TOKEN) {
65 | token = await Token.at(ERC20_TOKEN)
66 | usdtToken = await USDTToken.at(ERC20_TOKEN)
67 | } else {
68 | token = await Token.deployed()
69 | await token.mint(sender, tokenDenomination)
70 | }
71 | badRecipient = await BadRecipient.new()
72 | snapshotId = await takeSnapshot()
73 | groth16 = await buildGroth16()
74 | circuit = require('../build/circuits/withdraw.json')
75 | proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
76 | })
77 |
78 | describe('#constructor', () => {
79 | it('should initialize', async () => {
80 | const tokenFromContract = await tornado.token()
81 | tokenFromContract.should.be.equal(token.address)
82 | })
83 | })
84 |
85 | describe('#deposit', () => {
86 | it('should work', async () => {
87 | const commitment = toFixedHex(43)
88 | await token.approve(tornado.address, tokenDenomination)
89 |
90 | let { logs } = await tornado.deposit(commitment, { from: sender })
91 |
92 | logs[0].event.should.be.equal('Deposit')
93 | logs[0].args.commitment.should.be.equal(commitment)
94 | logs[0].args.leafIndex.should.be.eq.BN(0)
95 | })
96 |
97 | it('should not allow to send ether on deposit', async () => {
98 | const commitment = toFixedHex(43)
99 | await token.approve(tornado.address, tokenDenomination)
100 |
101 | let error = await tornado.deposit(commitment, { from: sender, value: 1e6 }).should.be.rejected
102 | error.reason.should.be.equal('ETH value is supposed to be 0 for ERC20 instance')
103 | })
104 | })
105 |
106 | describe('#withdraw', () => {
107 | it('should work', async () => {
108 | const deposit = generateDeposit()
109 | const user = accounts[4]
110 | tree.insert(deposit.commitment)
111 | await token.mint(user, tokenDenomination)
112 |
113 | const balanceUserBefore = await token.balanceOf(user)
114 | await token.approve(tornado.address, tokenDenomination, { from: user })
115 | // Uncomment to measure gas usage
116 | // let gas = await tornado.deposit.estimateGas(toBN(deposit.commitment.toString()), { from: user, gasPrice: '0' })
117 | // console.log('deposit gas:', gas)
118 | await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
119 |
120 | const balanceUserAfter = await token.balanceOf(user)
121 | balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
122 |
123 | const { pathElements, pathIndices } = tree.path(0)
124 | // Circuit input
125 | const input = stringifyBigInts({
126 | // public
127 | root: tree.root(),
128 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
129 | relayer,
130 | recipient,
131 | fee,
132 | refund,
133 |
134 | // private
135 | nullifier: deposit.nullifier,
136 | secret: deposit.secret,
137 | pathElements: pathElements,
138 | pathIndices: pathIndices,
139 | })
140 |
141 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
142 | const { proof } = websnarkUtils.toSolidityInput(proofData)
143 |
144 | const balanceTornadoBefore = await token.balanceOf(tornado.address)
145 | const balanceRelayerBefore = await token.balanceOf(relayer)
146 | const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20))
147 |
148 | const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
149 | const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
150 | const ethBalanceRelayerBefore = await web3.eth.getBalance(relayer)
151 | let isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
152 | isSpent.should.be.equal(false)
153 | // Uncomment to measure gas usage
154 | // gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
155 | // console.log('withdraw gas:', gas)
156 | const args = [
157 | toFixedHex(input.root),
158 | toFixedHex(input.nullifierHash),
159 | toFixedHex(input.recipient, 20),
160 | toFixedHex(input.relayer, 20),
161 | toFixedHex(input.fee),
162 | toFixedHex(input.refund),
163 | ]
164 | const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
165 |
166 | const balanceTornadoAfter = await token.balanceOf(tornado.address)
167 | const balanceRelayerAfter = await token.balanceOf(relayer)
168 | const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
169 | const balanceReceiverAfter = await token.balanceOf(toFixedHex(recipient, 20))
170 | const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
171 | const ethBalanceRelayerAfter = await web3.eth.getBalance(relayer)
172 | const feeBN = toBN(fee.toString())
173 | balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(tokenDenomination)))
174 | balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore).add(feeBN))
175 | balanceReceiverAfter.should.be.eq.BN(
176 | toBN(balanceReceiverBefore).add(toBN(tokenDenomination).sub(feeBN)),
177 | )
178 |
179 | ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore))
180 | ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore).add(toBN(refund)))
181 | ethBalanceRelayerAfter.should.be.eq.BN(toBN(ethBalanceRelayerBefore).sub(toBN(refund)))
182 |
183 | logs[0].event.should.be.equal('Withdrawal')
184 | logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash))
185 | logs[0].args.relayer.should.be.eq.BN(relayer)
186 | logs[0].args.fee.should.be.eq.BN(feeBN)
187 | isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
188 | isSpent.should.be.equal(true)
189 | })
190 |
191 | it('should return refund to the relayer is case of fail', async () => {
192 | const deposit = generateDeposit()
193 | const user = accounts[4]
194 | recipient = bigInt(badRecipient.address)
195 | tree.insert(deposit.commitment)
196 | await token.mint(user, tokenDenomination)
197 |
198 | const balanceUserBefore = await token.balanceOf(user)
199 | await token.approve(tornado.address, tokenDenomination, { from: user })
200 | await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
201 |
202 | const balanceUserAfter = await token.balanceOf(user)
203 | balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
204 |
205 | const { pathElements, pathIndices } = tree.path(0)
206 | // Circuit input
207 | const input = stringifyBigInts({
208 | // public
209 | root: tree.root(),
210 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
211 | relayer,
212 | recipient,
213 | fee,
214 | refund,
215 |
216 | // private
217 | nullifier: deposit.nullifier,
218 | secret: deposit.secret,
219 | pathElements: pathElements,
220 | pathIndices: pathIndices,
221 | })
222 |
223 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
224 | const { proof } = websnarkUtils.toSolidityInput(proofData)
225 |
226 | const balanceTornadoBefore = await token.balanceOf(tornado.address)
227 | const balanceRelayerBefore = await token.balanceOf(relayer)
228 | const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20))
229 |
230 | const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
231 | const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
232 | const ethBalanceRelayerBefore = await web3.eth.getBalance(relayer)
233 | let isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
234 | isSpent.should.be.equal(false)
235 |
236 | const args = [
237 | toFixedHex(input.root),
238 | toFixedHex(input.nullifierHash),
239 | toFixedHex(input.recipient, 20),
240 | toFixedHex(input.relayer, 20),
241 | toFixedHex(input.fee),
242 | toFixedHex(input.refund),
243 | ]
244 | const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
245 |
246 | const balanceTornadoAfter = await token.balanceOf(tornado.address)
247 | const balanceRelayerAfter = await token.balanceOf(relayer)
248 | const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
249 | const balanceReceiverAfter = await token.balanceOf(toFixedHex(recipient, 20))
250 | const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
251 | const ethBalanceRelayerAfter = await web3.eth.getBalance(relayer)
252 | const feeBN = toBN(fee.toString())
253 | balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(tokenDenomination)))
254 | balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore).add(feeBN))
255 | balanceReceiverAfter.should.be.eq.BN(
256 | toBN(balanceReceiverBefore).add(toBN(tokenDenomination).sub(feeBN)),
257 | )
258 |
259 | ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore))
260 | ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore))
261 | ethBalanceRelayerAfter.should.be.eq.BN(toBN(ethBalanceRelayerBefore))
262 |
263 | logs[0].event.should.be.equal('Withdrawal')
264 | logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash))
265 | logs[0].args.relayer.should.be.eq.BN(relayer)
266 | logs[0].args.fee.should.be.eq.BN(feeBN)
267 | isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
268 | isSpent.should.be.equal(true)
269 | })
270 |
271 | it('should reject with wrong refund value', async () => {
272 | const deposit = generateDeposit()
273 | const user = accounts[4]
274 | tree.insert(deposit.commitment)
275 | await token.mint(user, tokenDenomination)
276 | await token.approve(tornado.address, tokenDenomination, { from: user })
277 | await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
278 |
279 | const { pathElements, pathIndices } = tree.path(0)
280 | // Circuit input
281 | const input = stringifyBigInts({
282 | // public
283 | root: tree.root(),
284 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
285 | relayer,
286 | recipient,
287 | fee,
288 | refund,
289 |
290 | // private
291 | nullifier: deposit.nullifier,
292 | secret: deposit.secret,
293 | pathElements: pathElements,
294 | pathIndices: pathIndices,
295 | })
296 |
297 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
298 | const { proof } = websnarkUtils.toSolidityInput(proofData)
299 |
300 | const args = [
301 | toFixedHex(input.root),
302 | toFixedHex(input.nullifierHash),
303 | toFixedHex(input.recipient, 20),
304 | toFixedHex(input.relayer, 20),
305 | toFixedHex(input.fee),
306 | toFixedHex(input.refund),
307 | ]
308 | let { reason } = await tornado.withdraw(proof, ...args, { value: 1, from: relayer, gasPrice: '0' })
309 | .should.be.rejected
310 | reason.should.be.equal('Incorrect refund amount received by the contract')
311 | ;({ reason } = await tornado.withdraw(proof, ...args, {
312 | value: toBN(refund).mul(toBN(2)),
313 | from: relayer,
314 | gasPrice: '0',
315 | }).should.be.rejected)
316 | reason.should.be.equal('Incorrect refund amount received by the contract')
317 | })
318 |
319 | it.skip('should work with REAL USDT', async () => {
320 | // dont forget to specify your token in .env
321 | // USDT decimals is 6, so TOKEN_AMOUNT=1000000
322 | // and sent `tokenDenomination` to accounts[0] (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1)
323 | // run ganache as
324 | // ganache-cli --fork https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448@13147586 -d --keepAliveTimeout 20
325 | const deposit = generateDeposit()
326 | const user = accounts[4]
327 | const userBal = await usdtToken.balanceOf(user)
328 | console.log('userBal', userBal.toString())
329 | const senderBal = await usdtToken.balanceOf(sender)
330 | console.log('senderBal', senderBal.toString())
331 | tree.insert(deposit.commitment)
332 | await usdtToken.transfer(user, tokenDenomination, { from: sender })
333 | console.log('transfer done')
334 |
335 | const balanceUserBefore = await usdtToken.balanceOf(user)
336 | console.log('balanceUserBefore', balanceUserBefore.toString())
337 | await usdtToken.approve(tornado.address, tokenDenomination, { from: user })
338 | console.log('approve done')
339 | const allowanceUser = await usdtToken.allowance(user, tornado.address)
340 | console.log('allowanceUser', allowanceUser.toString())
341 | await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
342 | console.log('deposit done')
343 |
344 | const balanceUserAfter = await usdtToken.balanceOf(user)
345 | balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
346 |
347 | const { pathElements, pathIndices } = tree.path(0)
348 |
349 | // Circuit input
350 | const input = stringifyBigInts({
351 | // public
352 | root: tree.root(),
353 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
354 | relayer: operator,
355 | recipient,
356 | fee,
357 | refund,
358 |
359 | // private
360 | nullifier: deposit.nullifier,
361 | secret: deposit.secret,
362 | pathElements: pathElements,
363 | pathIndices: pathIndices,
364 | })
365 |
366 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
367 | const { proof } = websnarkUtils.toSolidityInput(proofData)
368 |
369 | const balanceTornadoBefore = await usdtToken.balanceOf(tornado.address)
370 | const balanceRelayerBefore = await usdtToken.balanceOf(relayer)
371 | const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
372 | const balanceReceiverBefore = await usdtToken.balanceOf(toFixedHex(recipient, 20))
373 | const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
374 | let isSpent = await tornado.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
375 | isSpent.should.be.equal(false)
376 |
377 | // Uncomment to measure gas usage
378 | // gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
379 | // console.log('withdraw gas:', gas)
380 | const args = [
381 | toFixedHex(input.root),
382 | toFixedHex(input.nullifierHash),
383 | toFixedHex(input.recipient, 20),
384 | toFixedHex(input.relayer, 20),
385 | toFixedHex(input.fee),
386 | toFixedHex(input.refund),
387 | ]
388 | const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
389 |
390 | const balanceTornadoAfter = await usdtToken.balanceOf(tornado.address)
391 | const balanceRelayerAfter = await usdtToken.balanceOf(relayer)
392 | const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
393 | const balanceReceiverAfter = await usdtToken.balanceOf(toFixedHex(recipient, 20))
394 | const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
395 | const feeBN = toBN(fee.toString())
396 | balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(tokenDenomination)))
397 | balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
398 | ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore).add(feeBN))
399 | balanceReceiverAfter.should.be.eq.BN(toBN(balanceReceiverBefore).add(toBN(tokenDenomination)))
400 | ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore).add(toBN(refund)).sub(feeBN))
401 |
402 | logs[0].event.should.be.equal('Withdrawal')
403 | logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString()))
404 | logs[0].args.relayer.should.be.eq.BN(operator)
405 | logs[0].args.fee.should.be.eq.BN(feeBN)
406 | isSpent = await tornado.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
407 | isSpent.should.be.equal(true)
408 | })
409 | it.skip('should work with REAL DAI', async () => {
410 | // dont forget to specify your token in .env
411 | // and send `tokenDenomination` to accounts[0] (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1)
412 | // run ganache as
413 | // npx ganache-cli --fork https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448@13146218 -d --keepAliveTimeout 20
414 | const deposit = generateDeposit()
415 | const user = accounts[4]
416 | const userBal = await token.balanceOf(user)
417 | console.log('userBal', userBal.toString())
418 | const senderBal = await token.balanceOf(sender)
419 | console.log('senderBal', senderBal.toString())
420 | tree.insert(deposit.commitment)
421 | await token.transfer(user, tokenDenomination, { from: sender })
422 | console.log('transfer done')
423 |
424 | const balanceUserBefore = await token.balanceOf(user)
425 | console.log('balanceUserBefore', balanceUserBefore.toString())
426 | await token.approve(tornado.address, tokenDenomination, { from: user })
427 | console.log('approve done')
428 | await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
429 | console.log('deposit done')
430 |
431 | const balanceUserAfter = await token.balanceOf(user)
432 | balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
433 |
434 | const { pathElements, pathIndices } = tree.path(0)
435 |
436 | // Circuit input
437 | const input = stringifyBigInts({
438 | // public
439 | root: tree.root(),
440 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
441 | relayer: operator,
442 | recipient,
443 | fee,
444 | refund,
445 |
446 | // private
447 | nullifier: deposit.nullifier,
448 | secret: deposit.secret,
449 | pathElements: pathElements,
450 | pathIndices: pathIndices,
451 | })
452 |
453 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
454 | const { proof } = websnarkUtils.toSolidityInput(proofData)
455 |
456 | const balanceTornadoBefore = await token.balanceOf(tornado.address)
457 | const balanceRelayerBefore = await token.balanceOf(relayer)
458 | const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
459 | const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20))
460 | const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
461 | let isSpent = await tornado.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
462 | isSpent.should.be.equal(false)
463 |
464 | // Uncomment to measure gas usage
465 | // gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
466 | // console.log('withdraw gas:', gas)
467 | const args = [
468 | toFixedHex(input.root),
469 | toFixedHex(input.nullifierHash),
470 | toFixedHex(input.recipient, 20),
471 | toFixedHex(input.relayer, 20),
472 | toFixedHex(input.fee),
473 | toFixedHex(input.refund),
474 | ]
475 | const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
476 | console.log('withdraw done')
477 |
478 | const balanceTornadoAfter = await token.balanceOf(tornado.address)
479 | const balanceRelayerAfter = await token.balanceOf(relayer)
480 | const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
481 | const balanceReceiverAfter = await token.balanceOf(toFixedHex(recipient, 20))
482 | const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
483 | const feeBN = toBN(fee.toString())
484 | balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(tokenDenomination)))
485 | balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
486 | ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore).add(feeBN))
487 | balanceReceiverAfter.should.be.eq.BN(toBN(balanceReceiverBefore).add(toBN(tokenDenomination)))
488 | ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore).add(toBN(refund)).sub(feeBN))
489 |
490 | logs[0].event.should.be.equal('Withdrawal')
491 | logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString()))
492 | logs[0].args.relayer.should.be.eq.BN(operator)
493 | logs[0].args.fee.should.be.eq.BN(feeBN)
494 | isSpent = await tornado.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
495 | isSpent.should.be.equal(true)
496 | })
497 | })
498 |
499 | afterEach(async () => {
500 | await revertSnapshot(snapshotId.result)
501 | // eslint-disable-next-line require-atomic-updates
502 | snapshotId = await takeSnapshot()
503 | tree = new MerkleTree(levels)
504 | })
505 | })
506 |
--------------------------------------------------------------------------------
/test/ETHTornado.test.js:
--------------------------------------------------------------------------------
1 | /* global artifacts, web3, contract */
2 | require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
3 | const fs = require('fs')
4 |
5 | const { toBN, randomHex } = require('web3-utils')
6 | const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
7 |
8 | const Tornado = artifacts.require('./ETHTornado.sol')
9 | const { ETH_AMOUNT, MERKLE_TREE_HEIGHT } = process.env
10 |
11 | const websnarkUtils = require('websnark/src/utils')
12 | const buildGroth16 = require('websnark/src/groth16')
13 | const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts
14 | const unstringifyBigInts2 = require('snarkjs/src/stringifybigint').unstringifyBigInts
15 | const snarkjs = require('snarkjs')
16 | const bigInt = snarkjs.bigInt
17 | const crypto = require('crypto')
18 | const circomlib = require('circomlib')
19 | const MerkleTree = require('fixed-merkle-tree')
20 |
21 | const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
22 | const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
23 | const toFixedHex = (number, length = 32) =>
24 | '0x' +
25 | bigInt(number)
26 | .toString(16)
27 | .padStart(length * 2, '0')
28 | const getRandomRecipient = () => rbigint(20)
29 |
30 | function generateDeposit() {
31 | let deposit = {
32 | secret: rbigint(31),
33 | nullifier: rbigint(31),
34 | }
35 | const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
36 | deposit.commitment = pedersenHash(preimage)
37 | return deposit
38 | }
39 |
40 | // eslint-disable-next-line no-unused-vars
41 | function BNArrayToStringArray(array) {
42 | const arrayToPrint = []
43 | array.forEach((item) => {
44 | arrayToPrint.push(item.toString())
45 | })
46 | return arrayToPrint
47 | }
48 |
49 | function snarkVerify(proof) {
50 | proof = unstringifyBigInts2(proof)
51 | const verification_key = unstringifyBigInts2(require('../build/circuits/withdraw_verification_key.json'))
52 | return snarkjs['groth'].isValid(verification_key, proof, proof.publicSignals)
53 | }
54 |
55 | contract('ETHTornado', (accounts) => {
56 | let tornado
57 | const sender = accounts[0]
58 | const operator = accounts[0]
59 | const levels = MERKLE_TREE_HEIGHT || 16
60 | const value = ETH_AMOUNT || '1000000000000000000' // 1 ether
61 | let snapshotId
62 | let tree
63 | const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
64 | const refund = bigInt(0)
65 | const recipient = getRandomRecipient()
66 | const relayer = accounts[1]
67 | let groth16
68 | let circuit
69 | let proving_key
70 |
71 | before(async () => {
72 | tree = new MerkleTree(levels)
73 | tornado = await Tornado.deployed()
74 | snapshotId = await takeSnapshot()
75 | groth16 = await buildGroth16()
76 | circuit = require('../build/circuits/withdraw.json')
77 | proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
78 | })
79 |
80 | describe('#constructor', () => {
81 | it('should initialize', async () => {
82 | const etherDenomination = await tornado.denomination()
83 | etherDenomination.should.be.eq.BN(toBN(value))
84 | })
85 | })
86 |
87 | describe('#deposit', () => {
88 | it('should emit event', async () => {
89 | let commitment = toFixedHex(42)
90 | let { logs } = await tornado.deposit(commitment, { value, from: sender })
91 |
92 | logs[0].event.should.be.equal('Deposit')
93 | logs[0].args.commitment.should.be.equal(commitment)
94 | logs[0].args.leafIndex.should.be.eq.BN(0)
95 |
96 | commitment = toFixedHex(12)
97 | ;({ logs } = await tornado.deposit(commitment, { value, from: accounts[2] }))
98 |
99 | logs[0].event.should.be.equal('Deposit')
100 | logs[0].args.commitment.should.be.equal(commitment)
101 | logs[0].args.leafIndex.should.be.eq.BN(1)
102 | })
103 |
104 | it('should throw if there is a such commitment', async () => {
105 | const commitment = toFixedHex(42)
106 | await tornado.deposit(commitment, { value, from: sender }).should.be.fulfilled
107 | const error = await tornado.deposit(commitment, { value, from: sender }).should.be.rejected
108 | error.reason.should.be.equal('The commitment has been submitted')
109 | })
110 | })
111 |
112 | describe('snark proof verification on js side', () => {
113 | it('should detect tampering', async () => {
114 | const deposit = generateDeposit()
115 | tree.insert(deposit.commitment)
116 | const { pathElements, pathIndices } = tree.path(0)
117 |
118 | const input = stringifyBigInts({
119 | root: tree.root(),
120 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
121 | nullifier: deposit.nullifier,
122 | relayer: operator,
123 | recipient,
124 | fee,
125 | refund,
126 | secret: deposit.secret,
127 | pathElements: pathElements,
128 | pathIndices: pathIndices,
129 | })
130 |
131 | let proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
132 | const originalProof = JSON.parse(JSON.stringify(proofData))
133 | let result = snarkVerify(proofData)
134 | result.should.be.equal(true)
135 |
136 | // nullifier
137 | proofData.publicSignals[1] =
138 | '133792158246920651341275668520530514036799294649489851421007411546007850802'
139 | result = snarkVerify(proofData)
140 | result.should.be.equal(false)
141 | proofData = originalProof
142 |
143 | // try to cheat with recipient
144 | proofData.publicSignals[2] = '133738360804642228759657445999390850076318544422'
145 | result = snarkVerify(proofData)
146 | result.should.be.equal(false)
147 | proofData = originalProof
148 |
149 | // fee
150 | proofData.publicSignals[3] = '1337100000000000000000'
151 | result = snarkVerify(proofData)
152 | result.should.be.equal(false)
153 | proofData = originalProof
154 | })
155 | })
156 |
157 | describe('#withdraw', () => {
158 | it('should work', async () => {
159 | const deposit = generateDeposit()
160 | const user = accounts[4]
161 | tree.insert(deposit.commitment)
162 |
163 | const balanceUserBefore = await web3.eth.getBalance(user)
164 |
165 | // Uncomment to measure gas usage
166 | // let gas = await tornado.deposit.estimateGas(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
167 | // console.log('deposit gas:', gas)
168 | await tornado.deposit(toFixedHex(deposit.commitment), { value, from: user, gasPrice: '0' })
169 |
170 | const balanceUserAfter = await web3.eth.getBalance(user)
171 | balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(value)))
172 |
173 | const { pathElements, pathIndices } = tree.path(0)
174 |
175 | // Circuit input
176 | const input = stringifyBigInts({
177 | // public
178 | root: tree.root(),
179 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
180 | relayer: operator,
181 | recipient,
182 | fee,
183 | refund,
184 |
185 | // private
186 | nullifier: deposit.nullifier,
187 | secret: deposit.secret,
188 | pathElements: pathElements,
189 | pathIndices: pathIndices,
190 | })
191 |
192 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
193 | const { proof } = websnarkUtils.toSolidityInput(proofData)
194 |
195 | const balanceTornadoBefore = await web3.eth.getBalance(tornado.address)
196 | const balanceRelayerBefore = await web3.eth.getBalance(relayer)
197 | const balanceOperatorBefore = await web3.eth.getBalance(operator)
198 | const balanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
199 | let isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
200 | isSpent.should.be.equal(false)
201 |
202 | // Uncomment to measure gas usage
203 | // gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
204 | // console.log('withdraw gas:', gas)
205 | const args = [
206 | toFixedHex(input.root),
207 | toFixedHex(input.nullifierHash),
208 | toFixedHex(input.recipient, 20),
209 | toFixedHex(input.relayer, 20),
210 | toFixedHex(input.fee),
211 | toFixedHex(input.refund),
212 | ]
213 | const { logs } = await tornado.withdraw(proof, ...args, { from: relayer, gasPrice: '0' })
214 |
215 | const balanceTornadoAfter = await web3.eth.getBalance(tornado.address)
216 | const balanceRelayerAfter = await web3.eth.getBalance(relayer)
217 | const balanceOperatorAfter = await web3.eth.getBalance(operator)
218 | const balanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
219 | const feeBN = toBN(fee.toString())
220 | balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(value)))
221 | balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
222 | balanceOperatorAfter.should.be.eq.BN(toBN(balanceOperatorBefore).add(feeBN))
223 | balanceReceiverAfter.should.be.eq.BN(toBN(balanceReceiverBefore).add(toBN(value)).sub(feeBN))
224 |
225 | logs[0].event.should.be.equal('Withdrawal')
226 | logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash))
227 | logs[0].args.relayer.should.be.eq.BN(operator)
228 | logs[0].args.fee.should.be.eq.BN(feeBN)
229 | isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
230 | isSpent.should.be.equal(true)
231 | })
232 |
233 | it('should prevent double spend', async () => {
234 | const deposit = generateDeposit()
235 | tree.insert(deposit.commitment)
236 | await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
237 |
238 | const { pathElements, pathIndices } = tree.path(0)
239 |
240 | const input = stringifyBigInts({
241 | root: tree.root(),
242 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
243 | nullifier: deposit.nullifier,
244 | relayer: operator,
245 | recipient,
246 | fee,
247 | refund,
248 | secret: deposit.secret,
249 | pathElements: pathElements,
250 | pathIndices: pathIndices,
251 | })
252 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
253 | const { proof } = websnarkUtils.toSolidityInput(proofData)
254 | const args = [
255 | toFixedHex(input.root),
256 | toFixedHex(input.nullifierHash),
257 | toFixedHex(input.recipient, 20),
258 | toFixedHex(input.relayer, 20),
259 | toFixedHex(input.fee),
260 | toFixedHex(input.refund),
261 | ]
262 | await tornado.withdraw(proof, ...args, { from: relayer }).should.be.fulfilled
263 | const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
264 | error.reason.should.be.equal('The note has been already spent')
265 | })
266 |
267 | it('should prevent double spend with overflow', async () => {
268 | const deposit = generateDeposit()
269 | tree.insert(deposit.commitment)
270 | await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
271 |
272 | const { pathElements, pathIndices } = tree.path(0)
273 |
274 | const input = stringifyBigInts({
275 | root: tree.root(),
276 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
277 | nullifier: deposit.nullifier,
278 | relayer: operator,
279 | recipient,
280 | fee,
281 | refund,
282 | secret: deposit.secret,
283 | pathElements: pathElements,
284 | pathIndices: pathIndices,
285 | })
286 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
287 | const { proof } = websnarkUtils.toSolidityInput(proofData)
288 | const args = [
289 | toFixedHex(input.root),
290 | toFixedHex(
291 | toBN(input.nullifierHash).add(
292 | toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617'),
293 | ),
294 | ),
295 | toFixedHex(input.recipient, 20),
296 | toFixedHex(input.relayer, 20),
297 | toFixedHex(input.fee),
298 | toFixedHex(input.refund),
299 | ]
300 | const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
301 | error.reason.should.be.equal('verifier-gte-snark-scalar-field')
302 | })
303 |
304 | it('fee should be less or equal transfer value', async () => {
305 | const deposit = generateDeposit()
306 | tree.insert(deposit.commitment)
307 | await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
308 |
309 | const { pathElements, pathIndices } = tree.path(0)
310 | const largeFee = bigInt(value).add(bigInt(1))
311 | const input = stringifyBigInts({
312 | root: tree.root(),
313 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
314 | nullifier: deposit.nullifier,
315 | relayer: operator,
316 | recipient,
317 | fee: largeFee,
318 | refund,
319 | secret: deposit.secret,
320 | pathElements: pathElements,
321 | pathIndices: pathIndices,
322 | })
323 |
324 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
325 | const { proof } = websnarkUtils.toSolidityInput(proofData)
326 | const args = [
327 | toFixedHex(input.root),
328 | toFixedHex(input.nullifierHash),
329 | toFixedHex(input.recipient, 20),
330 | toFixedHex(input.relayer, 20),
331 | toFixedHex(input.fee),
332 | toFixedHex(input.refund),
333 | ]
334 | const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
335 | error.reason.should.be.equal('Fee exceeds transfer value')
336 | })
337 |
338 | it('should throw for corrupted merkle tree root', async () => {
339 | const deposit = generateDeposit()
340 | tree.insert(deposit.commitment)
341 | await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
342 |
343 | const { pathElements, pathIndices } = tree.path(0)
344 |
345 | const input = stringifyBigInts({
346 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
347 | root: tree.root(),
348 | nullifier: deposit.nullifier,
349 | relayer: operator,
350 | recipient,
351 | fee,
352 | refund,
353 | secret: deposit.secret,
354 | pathElements: pathElements,
355 | pathIndices: pathIndices,
356 | })
357 |
358 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
359 | const { proof } = websnarkUtils.toSolidityInput(proofData)
360 |
361 | const args = [
362 | toFixedHex(randomHex(32)),
363 | toFixedHex(input.nullifierHash),
364 | toFixedHex(input.recipient, 20),
365 | toFixedHex(input.relayer, 20),
366 | toFixedHex(input.fee),
367 | toFixedHex(input.refund),
368 | ]
369 | const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
370 | error.reason.should.be.equal('Cannot find your merkle root')
371 | })
372 |
373 | it('should reject with tampered public inputs', async () => {
374 | const deposit = generateDeposit()
375 | tree.insert(deposit.commitment)
376 | await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
377 |
378 | let { pathElements, pathIndices } = tree.path(0)
379 |
380 | const input = stringifyBigInts({
381 | root: tree.root(),
382 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
383 | nullifier: deposit.nullifier,
384 | relayer: operator,
385 | recipient,
386 | fee,
387 | refund,
388 | secret: deposit.secret,
389 | pathElements: pathElements,
390 | pathIndices: pathIndices,
391 | })
392 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
393 | let { proof } = websnarkUtils.toSolidityInput(proofData)
394 | const args = [
395 | toFixedHex(input.root),
396 | toFixedHex(input.nullifierHash),
397 | toFixedHex(input.recipient, 20),
398 | toFixedHex(input.relayer, 20),
399 | toFixedHex(input.fee),
400 | toFixedHex(input.refund),
401 | ]
402 | let incorrectArgs
403 | const originalProof = proof.slice()
404 |
405 | // recipient
406 | incorrectArgs = [
407 | toFixedHex(input.root),
408 | toFixedHex(input.nullifierHash),
409 | toFixedHex('0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337', 20),
410 | toFixedHex(input.relayer, 20),
411 | toFixedHex(input.fee),
412 | toFixedHex(input.refund),
413 | ]
414 | let error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
415 | error.reason.should.be.equal('Invalid withdraw proof')
416 |
417 | // fee
418 | incorrectArgs = [
419 | toFixedHex(input.root),
420 | toFixedHex(input.nullifierHash),
421 | toFixedHex(input.recipient, 20),
422 | toFixedHex(input.relayer, 20),
423 | toFixedHex('0x000000000000000000000000000000000000000000000000015345785d8a0000'),
424 | toFixedHex(input.refund),
425 | ]
426 | error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
427 | error.reason.should.be.equal('Invalid withdraw proof')
428 |
429 | // nullifier
430 | incorrectArgs = [
431 | toFixedHex(input.root),
432 | toFixedHex('0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad'),
433 | toFixedHex(input.recipient, 20),
434 | toFixedHex(input.relayer, 20),
435 | toFixedHex(input.fee),
436 | toFixedHex(input.refund),
437 | ]
438 | error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
439 | error.reason.should.be.equal('Invalid withdraw proof')
440 |
441 | // proof itself
442 | proof = '0xbeef' + proof.substr(6)
443 | await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
444 |
445 | // should work with original values
446 | await tornado.withdraw(originalProof, ...args, { from: relayer }).should.be.fulfilled
447 | })
448 |
449 | it('should reject with non zero refund', async () => {
450 | const deposit = generateDeposit()
451 | tree.insert(deposit.commitment)
452 | await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
453 |
454 | const { pathElements, pathIndices } = tree.path(0)
455 |
456 | const input = stringifyBigInts({
457 | nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
458 | root: tree.root(),
459 | nullifier: deposit.nullifier,
460 | relayer: operator,
461 | recipient,
462 | fee,
463 | refund: bigInt(1),
464 | secret: deposit.secret,
465 | pathElements: pathElements,
466 | pathIndices: pathIndices,
467 | })
468 |
469 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
470 | const { proof } = websnarkUtils.toSolidityInput(proofData)
471 |
472 | const args = [
473 | toFixedHex(input.root),
474 | toFixedHex(input.nullifierHash),
475 | toFixedHex(input.recipient, 20),
476 | toFixedHex(input.relayer, 20),
477 | toFixedHex(input.fee),
478 | toFixedHex(input.refund),
479 | ]
480 | const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
481 | error.reason.should.be.equal('Refund value is supposed to be zero for ETH instance')
482 | })
483 | })
484 |
485 | describe('#isSpent', () => {
486 | it('should work', async () => {
487 | const deposit1 = generateDeposit()
488 | const deposit2 = generateDeposit()
489 | tree.insert(deposit1.commitment)
490 | tree.insert(deposit2.commitment)
491 | await tornado.deposit(toFixedHex(deposit1.commitment), { value, gasPrice: '0' })
492 | await tornado.deposit(toFixedHex(deposit2.commitment), { value, gasPrice: '0' })
493 |
494 | const { pathElements, pathIndices } = tree.path(1)
495 |
496 | // Circuit input
497 | const input = stringifyBigInts({
498 | // public
499 | root: tree.root(),
500 | nullifierHash: pedersenHash(deposit2.nullifier.leInt2Buff(31)),
501 | relayer: operator,
502 | recipient,
503 | fee,
504 | refund,
505 |
506 | // private
507 | nullifier: deposit2.nullifier,
508 | secret: deposit2.secret,
509 | pathElements: pathElements,
510 | pathIndices: pathIndices,
511 | })
512 |
513 | const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
514 | const { proof } = websnarkUtils.toSolidityInput(proofData)
515 |
516 | const args = [
517 | toFixedHex(input.root),
518 | toFixedHex(input.nullifierHash),
519 | toFixedHex(input.recipient, 20),
520 | toFixedHex(input.relayer, 20),
521 | toFixedHex(input.fee),
522 | toFixedHex(input.refund),
523 | ]
524 |
525 | await tornado.withdraw(proof, ...args, { from: relayer, gasPrice: '0' })
526 |
527 | const nullifierHash1 = toFixedHex(pedersenHash(deposit1.nullifier.leInt2Buff(31)))
528 | const nullifierHash2 = toFixedHex(pedersenHash(deposit2.nullifier.leInt2Buff(31)))
529 | const spentArray = await tornado.isSpentArray([nullifierHash1, nullifierHash2])
530 | spentArray.should.be.deep.equal([false, true])
531 | })
532 | })
533 |
534 | afterEach(async () => {
535 | await revertSnapshot(snapshotId.result)
536 | // eslint-disable-next-line require-atomic-updates
537 | snapshotId = await takeSnapshot()
538 | tree = new MerkleTree(levels)
539 | })
540 | })
541 |
--------------------------------------------------------------------------------
/test/MerkleTreeWithHistory.test.js:
--------------------------------------------------------------------------------
1 | /* global artifacts, web3, contract */
2 | require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
3 |
4 | const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
5 |
6 | const MerkleTreeWithHistory = artifacts.require('./MerkleTreeWithHistoryMock.sol')
7 | const hasherContract = artifacts.require('./Hasher.sol')
8 |
9 | const MerkleTree = require('fixed-merkle-tree')
10 |
11 | const snarkjs = require('snarkjs')
12 | const bigInt = snarkjs.bigInt
13 |
14 | const { ETH_AMOUNT, MERKLE_TREE_HEIGHT } = process.env
15 |
16 | // eslint-disable-next-line no-unused-vars
17 | function BNArrayToStringArray(array) {
18 | const arrayToPrint = []
19 | array.forEach((item) => {
20 | arrayToPrint.push(item.toString())
21 | })
22 | return arrayToPrint
23 | }
24 |
25 | function toFixedHex(number, length = 32) {
26 | let str = bigInt(number).toString(16)
27 | while (str.length < length * 2) str = '0' + str
28 | str = '0x' + str
29 | return str
30 | }
31 |
32 | contract('MerkleTreeWithHistory', (accounts) => {
33 | let merkleTreeWithHistory
34 | let hasherInstance
35 | let levels = MERKLE_TREE_HEIGHT || 16
36 | const sender = accounts[0]
37 | // eslint-disable-next-line no-unused-vars
38 | const value = ETH_AMOUNT || '1000000000000000000'
39 | let snapshotId
40 | let tree
41 |
42 | before(async () => {
43 | tree = new MerkleTree(levels)
44 | hasherInstance = await hasherContract.deployed()
45 | merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, hasherInstance.address)
46 | snapshotId = await takeSnapshot()
47 | })
48 |
49 | describe('#constructor', () => {
50 | it('should initialize', async () => {
51 | const zeroValue = await merkleTreeWithHistory.ZERO_VALUE()
52 | const firstSubtree = await merkleTreeWithHistory.filledSubtrees(0)
53 | firstSubtree.should.be.equal(toFixedHex(zeroValue))
54 | const firstZero = await merkleTreeWithHistory.zeros(0)
55 | firstZero.should.be.equal(toFixedHex(zeroValue))
56 | })
57 | })
58 |
59 | describe('#insert', () => {
60 | it('should insert', async () => {
61 | let rootFromContract
62 |
63 | for (let i = 1; i < 11; i++) {
64 | await merkleTreeWithHistory.insert(toFixedHex(i), { from: sender })
65 | tree.insert(i)
66 | rootFromContract = await merkleTreeWithHistory.getLastRoot()
67 | toFixedHex(tree.root()).should.be.equal(rootFromContract.toString())
68 | }
69 | })
70 |
71 | it('should reject if tree is full', async () => {
72 | const levels = 6
73 | const merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, hasherInstance.address)
74 |
75 | for (let i = 0; i < 2 ** levels; i++) {
76 | await merkleTreeWithHistory.insert(toFixedHex(i + 42)).should.be.fulfilled
77 | }
78 |
79 | let error = await merkleTreeWithHistory.insert(toFixedHex(1337)).should.be.rejected
80 | error.reason.should.be.equal('Merkle tree is full. No more leaves can be added')
81 |
82 | error = await merkleTreeWithHistory.insert(toFixedHex(1)).should.be.rejected
83 | error.reason.should.be.equal('Merkle tree is full. No more leaves can be added')
84 | })
85 |
86 | it.skip('hasher gas', async () => {
87 | const levels = 6
88 | const merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels)
89 | const zeroValue = await merkleTreeWithHistory.zeroValue()
90 |
91 | const gas = await merkleTreeWithHistory.hashLeftRight.estimateGas(zeroValue, zeroValue)
92 | console.log('gas', gas - 21000)
93 | })
94 | })
95 |
96 | describe('#isKnownRoot', () => {
97 | it('should work', async () => {
98 | for (let i = 1; i < 5; i++) {
99 | await merkleTreeWithHistory.insert(toFixedHex(i), { from: sender }).should.be.fulfilled
100 | await tree.insert(i)
101 | let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(tree.root()))
102 | isKnown.should.be.equal(true)
103 | }
104 |
105 | await merkleTreeWithHistory.insert(toFixedHex(42), { from: sender }).should.be.fulfilled
106 | // check outdated root
107 | let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(tree.root()))
108 | isKnown.should.be.equal(true)
109 | })
110 |
111 | it('should not return uninitialized roots', async () => {
112 | await merkleTreeWithHistory.insert(toFixedHex(42), { from: sender }).should.be.fulfilled
113 | let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(0))
114 | isKnown.should.be.equal(false)
115 | })
116 | })
117 |
118 | afterEach(async () => {
119 | await revertSnapshot(snapshotId.result)
120 | // eslint-disable-next-line require-atomic-updates
121 | snapshotId = await takeSnapshot()
122 | tree = new MerkleTree(levels)
123 | })
124 | })
125 |
--------------------------------------------------------------------------------
/truffle-config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const HDWalletProvider = require('@truffle/hdwallet-provider')
3 | const utils = require('web3-utils')
4 |
5 | module.exports = {
6 | /**
7 | * Networks define how you connect to your ethereum client and let you set the
8 | * defaults web3 uses to send transactions. If you don't specify one truffle
9 | * will spin up a development blockchain for you on port 9545 when you
10 | * run `develop` or `test`. You can ask a truffle command to use a specific
11 | * network from the command line, e.g
12 | *
13 | * $ truffle test --network
14 | */
15 |
16 | networks: {
17 | development: {
18 | host: '127.0.0.1', // Localhost (default: none)
19 | port: 8545, // Standard Ethereum port (default: none)
20 | network_id: '*', // Any network (default: none)
21 | },
22 | kovan: {
23 | provider: () =>
24 | new HDWalletProvider(
25 | process.env.PRIVATE_KEY,
26 | 'https://kovan.infura.io/v3/97c8bf358b9942a9853fab1ba93dc5b3',
27 | ),
28 | network_id: 42,
29 | gas: 6000000,
30 | gasPrice: utils.toWei('1', 'gwei'),
31 | // confirmations: 0,
32 | // timeoutBlocks: 200,
33 | skipDryRun: true,
34 | },
35 | goerli: {
36 | provider: () =>
37 | new HDWalletProvider(
38 | process.env.PRIVATE_KEY,
39 | 'https://goerli.infura.io/v3/d34c08f2cb7c4111b645d06ac7e35ba8',
40 | ),
41 | network_id: 5,
42 | gas: 6000000,
43 | gasPrice: utils.toWei('1', 'gwei'),
44 | // confirmations: 0,
45 | // timeoutBlocks: 200,
46 | skipDryRun: true,
47 | },
48 | rinkeby: {
49 | provider: () =>
50 | new HDWalletProvider(
51 | process.env.PRIVATE_KEY,
52 | 'https://rinkeby.infura.io/v3/97c8bf358b9942a9853fab1ba93dc5b3',
53 | ),
54 | network_id: 4,
55 | gas: 6000000,
56 | gasPrice: utils.toWei('1', 'gwei'),
57 | // confirmations: 0,
58 | // timeoutBlocks: 200,
59 | skipDryRun: true,
60 | },
61 | mainnet: {
62 | provider: () => new HDWalletProvider(process.env.PRIVATE_KEY, 'http://ethereum-rpc.trustwalletapp.com'),
63 | network_id: 1,
64 | gas: 6000000,
65 | gasPrice: utils.toWei('2', 'gwei'),
66 | // confirmations: 0,
67 | // timeoutBlocks: 200,
68 | skipDryRun: true,
69 | },
70 | },
71 |
72 | mocha: {
73 | // timeout: 100000
74 | },
75 |
76 | // Configure your compilers
77 | compilers: {
78 | solc: {
79 | version: '0.7.6',
80 | settings: {
81 | optimizer: {
82 | enabled: true,
83 | runs: 200,
84 | },
85 | },
86 | },
87 | external: {
88 | command: 'node ./scripts/compileHasher.js',
89 | targets: [
90 | {
91 | path: './build/Hasher.json',
92 | },
93 | ],
94 | },
95 | },
96 |
97 | plugins: ['solidity-coverage'],
98 | }
99 |
--------------------------------------------------------------------------------