Compare commits

...

215 Commits
1.0 ... master

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

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

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

- Remove usage of @truffle/artifactor in migration, since the artifact
  will now already exist
2020-04-29 02:07:54 -04:00
Alexey a533ad9ffb Add explicit constrains for recepient, relayer and fee 2020-04-28 10:36:48 +03:00
poma d4e6031982
fix cli 2020-04-17 21:54:27 +03:00
Alexey 2d9677831a fix cli 2020-04-15 16:14:05 +03:00
Alexey f04ff2b6fd update infura keys 2020-04-14 12:22:15 +03:00
Alexey 55e50fee3e websnark update 2020-04-13 22:03:04 +03:00
Alexey b0bca7fc36 fix solidity version 2020-04-13 22:02:21 +03:00
Alexey 54bb4c4b3c update links 2020-04-06 13:20:58 +03:00
Alexey 411098b589 pin snarkjs versions in dependencies 2020-04-06 13:11:42 +03:00
Alexey c8adb6b200 update circomlib 2020-04-06 11:58:17 +03:00
Alexey cfaf325c47 fix minimal example 2020-03-04 14:30:26 +03:00
Pertsev Alexey 30b07f76a5
Merge pull request #41 from tornadocash/demo
Demo
2020-03-04 14:14:40 +03:00
Pertsev Alexey d1c4a9bee6
Merge pull request #40 from tornadocash/networks
CLI update
2020-03-04 14:13:40 +03:00
Alexey 6a592154d2 make it work with ganache 2020-02-28 13:42:46 +03:00
poma 49bdd1bb6f
minimal demo wip 2020-02-28 13:22:08 +03:00
Alexey 0e0ac72b0f readme fix 2020-02-27 16:40:21 +03:00
Alexey 3e9df20f35 tidy 2020-02-27 16:36:44 +03:00
Alexey 62c7951961 fix browser version; readme update 2020-02-27 16:32:10 +03:00
Alexey 8afd208765 fix bugs 2020-02-27 10:46:43 +03:00
Alexey ff71072700 WIP 2020-02-26 16:57:53 +03:00
Alexey 27316a1edd poma WIP 2020-02-25 11:04:13 +03:00
Alexey 7033f34434 update WP link 2019-12-17 12:19:34 +03:00
Roman Semenov 91e652ae66
add whitepaper link 2019-12-17 15:52:14 +07:00
Alexey a780956a63 Rename 2019-12-14 12:44:18 +03:00
poma 0fc3cda775
remove migration script (moving to a separate branch) 2019-12-14 15:23:59 +07:00
poma 079ba3aa5a
update readme 2019-12-14 15:21:26 +07:00
Roman Semenov 0ac236d439
Merge pull request #33 from tornadocash/circomlib_audit
new circomlib update
2019-12-14 12:56:12 +07:00
Roman Storm 9757978d27 new circomlib update 2019-12-13 13:18:16 -08:00
Roman Storm 51c3d6b28c
Merge pull request #32 from tornadocash/migrationScript
Migration script
2019-12-13 13:15:06 -08:00
Alexey 6cd0ae8d87 fix cli 2019-12-13 23:36:32 +03:00
Alexey 68a861ed35 Merge branch 'master' into migrationScript 2019-12-13 23:21:25 +03:00
Alexey ea93af3243 Merge branch 'master' into migrationScript 2019-12-13 23:12:49 +03:00
Alexey bb3c4aae52 fix cli.js 2019-12-13 23:01:38 +03:00
Roman Storm 4a5921d8c2
Merge pull request #31 from tornadocash/rename
rename variables
2019-12-13 11:04:05 -08:00
poma 4114f7b52c
rename variables 2019-12-13 20:49:19 +07:00
Pertsev Alexey 8623e4092a
Merge pull request #30 from tornadocash/migration-temp
Migration temp
2019-12-11 23:08:12 +03:00
Pertsev Alexey 2671aab17b
Merge branch 'migrationScript' into migration-temp 2019-12-11 23:07:59 +03:00
Alexey 1be468c863 done 2019-12-11 23:04:38 +03:00
Alexey 265540067a WIP 2019-12-11 20:05:28 +03:00
poma 917892ef13
wip 2019-12-11 23:49:43 +07:00
Alexey 2c28e1d5aa tidy 2019-12-11 17:01:34 +03:00
poma b6b8aa9619
fix subtrees 2019-12-11 19:30:25 +07:00
poma ae44615198
make finishMigration payable 2019-12-11 19:29:03 +07:00
poma 1515959a01
change tree depth to 20 2019-12-11 19:29:03 +07:00
Roman Storm 5140e1b38f finish migration 2019-12-10 07:48:11 -08:00
Roman Semenov 6ae2fe612c Update README.md 2019-12-09 23:40:10 -08:00
poma 2a0f0ccfd9 remove unused import 2019-12-09 23:40:10 -08:00
poma 0e9732625e fix address formatting in tests 2019-12-09 23:40:10 -08:00
Roman Storm 656673b690 add migration script 2019-12-09 23:40:10 -08:00
Roman Semenov d9f4b16076
Update README.md 2019-12-05 22:52:01 +07:00
Pertsev Alexey beceeaae6e
Merge pull request #28 from peppersec/fix-test
fix address formatting in tests
2019-12-02 11:45:24 +03:00
poma e83f528f6f
remove unused import 2019-11-28 15:04:09 +07:00
Roman Semenov 62a9814aae
Merge pull request #27 from peppersec/isSpentPR
add isSpendArray view function
2019-11-28 11:54:28 +07:00
poma 2962f30ed5
fix address formatting in tests 2019-11-28 11:52:17 +07:00
Roman Storm a6cda4a501 add isSpendArray view function 2019-11-27 19:45:52 -08:00
Pertsev Alexey f5486f0943
Merge pull request #25 from peppersec/circuit-fix
check selector in mixer
2019-11-25 19:47:19 +03:00
Pertsev Alexey 2de4f9c721
Merge pull request #26 from peppersec/audit-circuit-fix-2
Audit circuit fix 2
2019-11-25 19:46:25 +03:00
poma a58d60623b
rename MerkleTree to make more clear that it has no output 2019-11-24 13:31:02 +04:00
poma 0c4c27b7b6
remove `private` modifiers from non-main templates 2019-11-24 13:30:58 +04:00
poma c5d1ef7734
check selector in mixer 2019-11-20 20:27:08 +03:00
Roman Storm d0e312eb80
Merge pull request #24 from peppersec/audit-6
Audit 6
2019-11-18 12:03:44 -08:00
Roman Storm 93f8a8943e
Merge pull request #23 from peppersec/audit-5
Audit 5
2019-11-15 12:57:29 -08:00
poma 24a21ac88f
fix test 2019-11-15 22:42:59 +03:00
poma c0e81f2a37
rename multiplexor 2019-11-15 11:44:07 +03:00
poma e6a4208b52
change zero value constant 2019-11-15 11:44:07 +03:00
Alexey ce550eea58 return refund to a relayer in case of failure 2019-11-15 10:59:06 +03:00
Alexey a6519fb280 move nonReentrant modifier 2019-11-14 20:49:34 +03:00
Alexey 0393ee9c05 fix test 2019-11-14 20:41:31 +03:00
Alexey 5b60e44a7e debug message fix 2019-11-14 14:01:11 +03:00
Roman Storm df395187bf
Merge pull request #22 from peppersec/cli-refactor
Cli refactor, relay support
2019-11-11 11:46:14 -08:00
Roman Storm dec0f4487c
Merge pull request #21 from peppersec/remove-toggleDeposits
remove toggleDeposits
2019-11-11 11:46:05 -08:00
Roman Storm a48858fd34
Merge pull request #20 from peppersec/audit-4
ether sending
2019-11-11 11:45:56 -08:00
Alexey 83c9ba7296 nonReentrant guards 2019-11-11 19:12:17 +03:00
poma d01018db9f support relay in CLI, refactor 2019-11-10 03:48:09 +03:00
poma 1a17e7eea3 make sure pathIndex is always a number (fixes bug with string concat instead of integer addition) 2019-11-10 03:46:27 +03:00
poma 183847ad67 remove toggleDeposits 2019-11-09 02:48:35 +03:00
Alexey e6cce0c7ce ether sending 2019-11-08 14:41:39 +03:00
Roman Semenov bc8d0b20fc
Merge pull request #19 from peppersec/audit-3
Audit fixes (batch 3)
2019-11-08 13:30:25 +03:00
poma 313713a061 fix tests 2019-11-08 07:39:22 +03:00
poma a94281ccc5 update dependencies 2019-11-08 04:11:29 +03:00
poma c4dded8a20 receiver -> recipient 2019-11-08 02:58:24 +03:00
poma 4d6dca78b2 make withdraw function external 2019-11-08 02:58:22 +03:00
poma 61864ceda1 reorder vars 2019-11-08 02:58:12 +03:00
poma ac8fc08cc2 use bytes32 for hashes 2019-11-08 02:57:39 +03:00
poma 74913e67b2 typed withdraw inputs 2019-11-08 02:55:08 +03:00
poma e9c2055bb4 changed merkleTree vars to uint32 where appropriate 2019-11-08 02:53:37 +03:00
poma f783b45559 refactor isKnownRoot, add test 2019-11-08 02:52:54 +03:00
poma e710b243d7 change error text 2019-11-08 02:46:33 +03:00
poma 35f4b031f4 rinkeby deployment 2019-11-08 02:46:28 +03:00
poma 3169d79ee0 add traceTransaction 2019-11-08 02:46:26 +03:00
Roman Semenov b000e66899
Merge pull request #15 from peppersec/audit-circuit
Circuit audit fixes
2019-11-06 12:31:23 +03:00
Roman Semenov fce4b1854c
Merge pull request #18 from peppersec/audit-2
Code style fixes for audit (batch 2)
2019-11-06 01:09:46 +03:00
Roman Semenov a0a4050211
Merge pull request #17 from peppersec/audit-1
Code style fixes for audit (batch 1)
2019-11-06 01:09:22 +03:00
Alexey d5b16547f7 tidy 2019-11-05 12:22:25 +03:00
poma 1fd0c7fdea change zero to local variable 2019-11-04 22:46:19 +03:00
poma 27e3121bb0 comments 2019-11-04 22:45:56 +03:00
poma ae889b5ad2 use abi.decode in token transfer 2019-11-04 22:44:19 +03:00
poma f8cd3fea1e refactor loop 2019-11-03 11:41:05 +03:00
poma c00e553299 check return data length for tokens 2019-11-03 11:25:58 +03:00
poma 8a179b9217 added comment on checks 2019-11-02 16:26:13 +03:00
poma 2ded1f8adb refactor merkle tree naming 2019-11-02 16:04:41 +03:00
poma e413ccdc29 make underscores consistent - in func args and in internal func names 2019-11-02 15:48:22 +03:00
poma 1fdabcc97c changed emptyElement to constant 2019-11-02 15:35:22 +03:00
poma 27a00bfd5f rename withdraw event 2019-11-02 14:50:37 +03:00
poma c92ac97ff2 make field_size constant, return extra addmod, refactor return 2019-11-02 14:30:00 +03:00
poma 91adb03131 add solidity version to IUSDT 2019-11-02 14:29:55 +03:00
poma c47408ebd7 cache current_zero value to prevent excessive SLOADs 2019-11-02 14:29:43 +03:00
poma 6095106549 rename current root index 2019-11-02 13:11:15 +03:00
poma 6571f54768 make _roots public 2019-11-02 13:11:03 +03:00
poma 1364762b93 make _roots constant sized array 2019-11-02 13:10:57 +03:00
poma 2bb751bfd1 change ROOT_HISTORY_SIZE type 2019-11-02 13:10:48 +03:00
poma 48cc57fad7 change arg name 2019-11-02 13:09:10 +03:00
poma 35500ac5bb change arg name 2019-11-02 13:09:02 +03:00
poma 8e8243823a make inheritable functions abstract 2019-11-02 13:08:41 +03:00
poma 111c966c1e add assert on denomination 2019-11-02 13:07:07 +03:00
poma 02e76a1ce6 pass verifier address as IVerifier 2019-11-02 13:06:10 +03:00
poma d019e48da3 inverted flags to reduce deploy cost, explicitly set state in toggleDeposits 2019-11-02 13:05:33 +03:00
poma 54e2c5f890 return inserted index from merkle tree 2019-11-02 11:40:46 +03:00
poma b8d22464e3 process deposit after changing state to prevent reentrancy 2019-11-02 11:40:27 +03:00
poma 1d258715e0 make operator not payable 2019-11-02 11:40:19 +03:00
poma b578213b0c check that eth value in ERC20 deposit is zero 2019-11-02 11:39:07 +03:00
poma bec65f217f allow fee equal to denomination 2019-11-02 11:24:16 +03:00
Roman Semenov bc134bbfe8
Merge pull request #13 from Agonical/patch-1
Update README.md
2019-11-02 05:10:01 +03:00
poma ec4508e81e rename pathIndex -> pathIndices 2019-11-02 05:05:25 +03:00
poma 07168f9816 refactor select() into a generic multiplexer 2019-11-02 04:55:25 +03:00
poma 7193655e49 make rounds number a constant 2019-11-02 04:33:19 +03:00
poma 9efab84e65 remove explicit constraints on inputs 2019-11-02 04:32:28 +03:00
Alexey 0484408e82 ERC20Mixer refund tests 2019-10-17 19:19:35 +03:00
Alexey a13a7306e2 ETHMixer refund test 2019-10-17 18:54:21 +03:00
Roman Semenov 63771560c6
Remove unused var 2019-10-17 16:57:39 +03:00
Roman Semenov 4ce8d1c2ce
fix refund 2019-10-17 16:35:23 +03:00
Agonical dcbf2ab693
Update README.md 2019-10-15 18:45:14 -07:00
poma 0cbc0ad79b refund asserts for ETH mixer 2019-10-07 07:15:06 +03:00
poma c889a88b4d fix tests, reorder constructor parameters 2019-10-06 08:36:01 +03:00
poma a77c04ea5a add refund mechanism for token withdrawal 2019-10-06 08:09:26 +03:00
poma c7f0ca9dfa add erc20 to integration test 2019-10-04 19:27:19 +03:00
poma 7a184d67d2 some fixes 2019-10-04 19:17:28 +03:00
poma 55b3644fd7 single argument proof 2019-10-04 18:20:47 +03:00
poma 6035255a49 tests, more docs, minor refactoring 2019-10-04 17:27:47 +03:00
poma 5c3c78e097 allow verifier keys update, and an option to permanently disable it 2019-10-04 16:43:15 +03:00
poma 3624cb7531 fix build command 2019-10-04 16:01:42 +03:00
poma 71b767ade1 rename mimc mentions to a generic hasher 2019-10-04 15:38:08 +03:00
poma 6b067f067f organize npm commands 2019-10-04 14:35:45 +03:00
poma bb80cd9788 update circom and snarkjs 2019-09-27 17:20:50 +03:00
Roman Storm 5ef6e33c78
Merge pull request #9 from peppersec/erc20_support
Erc20 support
2019-09-16 10:43:17 -07:00
Alexey 9e7aa186dc readme bug 2019-09-16 14:14:49 +03:00
Alexey 9132aeb6d5 max leaves count fix 2019-09-16 13:07:14 +03:00
Roman Storm a64f41a44e merge and add cli.js erc20 commands 2019-09-14 11:21:53 -07:00
Roman Storm 13e01755a6 WIP 2019-09-13 18:05:08 -07:00
Alexey 0689d76df1 leafs = 2**(levels - 1) 2019-09-11 11:25:32 +03:00
Alexey 3404acef48 fix leaf count 2019-09-11 11:17:32 +03:00
Alexey 3f4e686899 fix cli.js 2019-09-10 17:31:19 +03:00
Pertsev Alexey e75740becb
Merge branch 'master' into erc20_support 2019-09-10 17:28:01 +03:00
Pertsev Alexey 7ed13aa8e0
Merge pull request #10 from DryginAlexander/master
fix Merkle tree capacity
2019-09-10 17:00:02 +03:00
Alexey 010837f92b fix tests 2019-09-10 16:31:34 +03:00
poma 9009b9c56d
custom relayer 2019-09-06 23:20:49 -04:00
Roman Storm 50872ac342 change fee structure 2019-09-06 17:22:30 -04:00
poma d39eb3ca4b
add comments, improve readability, improve readme, fix default values 2019-09-05 17:43:06 -04:00
Drygin 374dd420f5 fix test of capacity of Merkle tree 2019-09-04 16:08:11 +03:00
Drygin ec30e2d357 fix merkle tree capacity 2019-09-04 13:31:10 +03:00
Alexey 0e0c87d533 CLI fix 2019-09-02 17:56:10 +03:00
Alexey e0ec575745 gas tuning 2019-08-30 13:21:53 +03:00
Alexey b8c0c1898f test with real DAI and USDT 2019-08-30 13:06:17 +03:00
Alexey 5006006a20 eslint vuln fix 2019-08-28 11:15:49 +03:00
Alexey 9f33aadd9d additional eth for the recipient 2019-08-27 23:42:24 +03:00
Alexey 0f5a5df522 withdraw test 2019-08-24 13:18:52 +03:00
Alexey b8142d03bb erc20 mixer support WIP 2019-08-20 23:39:21 +03:00
56 changed files with 16525 additions and 12615 deletions

View File

@ -1,5 +1,15 @@
MERKLE_TREE_HEIGHT=16
MERKLE_TREE_HEIGHT=20
# in wei
AMOUNT=1000000000000000000
EMPTY_ELEMENT=1337
ETH_AMOUNT=100000000000000000
TOKEN_AMOUNT=100000000000000000
PRIVATE_KEY=
ERC20_TOKEN=
# DAI mirror in Kovan
#ERC20_TOKEN=0xd2b1a6b34f4a68425e7c28b4db5a37be3b7a4947
# the block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some DAI is 13146218
# USDT mirror in Kovan
#ERC20_TOKEN=0xf3e0d7bf58c5d455d31ef1c2d5375904df525105
#TOKEN_AMOUNT=1000000
# the block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some USDT is 13147586

27
.eslintrc Normal file
View File

@ -0,0 +1,27 @@
{
"env": {
"node": true,
"browser": true,
"es6": true,
"mocha": true
},
"extends": ["eslint:recommended", "plugin:prettier/recommended", "prettier"],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": ["error", 2],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single"],
"semi": ["error", "never"],
"object-curly-spacing": ["error", "always"],
"comma-dangle": ["error", "always-multiline"],
"require-await": "error",
"prettier/prettier": ["error", { "printWidth": 110 }]
}
}

View File

@ -1,39 +0,0 @@
{
"env": {
"node": true,
"browser": true,
"es6": true,
"mocha": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"never"
],
"object-curly-spacing": [
"error",
"always"
],
"require-await": "error"
}
}

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

@ -0,0 +1,38 @@
name: build
on:
push:
branches: ['*']
tags: ['v[0-9]+.[0-9]+.[0-9]+']
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: yarn install
- run: yarn download
- run: cp .env.example .env
- run: npx ganache-cli > /dev/null &
- run: npm run migrate:dev
- run: yarn test
- run: node src/cli.js test
- run: yarn lint
- run: yarn coverage
- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Telegram Failure Notification
uses: appleboy/telegram-action@0.0.7
if: failure()
with:
message: ❗ Build failed for [${{ github.repository }}](https://github.com/${{ github.repository }}/actions) because of ${{ github.actor }}
format: markdown
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}

6
.gitignore vendored
View File

@ -1,7 +1,7 @@
build
.vscode
/index.js
Mixer_flat.sol
Tornado_flat.sol
# Created by .ignore support plugin (hsz.mobi)
### Node template
@ -94,3 +94,7 @@ typings/
# DynamoDB Local files
.dynamodb/
ERC20Tornado_flat.sol
ETHTornado_flat.sol
coverage.json

2
.nvmrc
View File

@ -1 +1 @@
11
12

9
.prettierignore Normal file
View File

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

16
.prettierrc Normal file
View File

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

View File

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

View File

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

186
README.md
View File

@ -1,47 +1,179 @@
# Tornado mixer [![Build Status](https://travis-ci.org/peppersec/tornado-mixer.svg?branch=master)](https://travis-ci.org/peppersec/tornado-mixer)
# Tornado Cash Privacy Solution [![build status](https://github.com/tornadocash/tornado-core/actions/workflows/build.yml/badge.svg)](https://github.com/tornadocash/tornado-core/actions/workflows/build.yml) [![Coverage Status](https://coveralls.io/repos/github/tornadocash/tornado-core/badge.svg?branch=master)](https://coveralls.io/github/tornadocash/tornado-core?branch=master)
![mixer image](./mixer.png)
Tornado Cash is a non-custodial Ethereum and ERC20 privacy solution based on zkSNARKs. It improves transaction privacy by breaking the on-chain link between the recipient and destination addresses. It uses a smart contract that accepts ETH deposits that can be withdrawn by a different address. Whenever ETH is withdrawn by the new address, there is no way to link the withdrawal to the deposit, ensuring complete privacy.
To make a deposit user generates a secret and sends its hash (called a commitment) along with the deposit amount to the Tornado smart contract. The contract accepts the deposit and adds the commitment to its list of deposits.
Later, the user decides to make a withdrawal. To do that, the user should provide a proof that he or she possesses a secret to an unspent commitment from the smart contracts list of deposits. zkSnark technology allows that to happen without revealing which exact deposit corresponds to this secret. The smart contract will check the proof and transfer deposited funds to the address specified for withdrawal. An external observer will be unable to determine which deposit this withdrawal came from.
You can read more about it in [this Medium article](https://medium.com/@tornado.cash/introducing-private-transactions-on-ethereum-now-42ee915babe0)
## Specs
- Deposit gas cost: deposit 888054
- Withdraw gas cost: 692133
- Circuit constraints: 22617
- Circuit proving time: 6116ms
- Deposit gas cost: 1088354 (43381 + 50859 \* tree_depth)
- Withdraw gas cost: 301233
- Circuit Constraints = 28271 (1869 + 1325 \* tree_depth)
- Circuit Proof time = 10213ms (1071 + 347 \* tree_depth)
- Serverless
## Security risks
* Cryptographic tools used by mixer (zkSNARKS, Pedersen commitment, MiMC hash) are not yet extensively audited by cryptographic experts and may be vulnerable
* Note: we use MiMC hash only for merkle tree, so even if a preimage attack on MiMC is discovered, it will not allow to deanonymize users. To drain funds attacker needs to be able to generate arbitrary hash collisions, which is a pretty strong assumption.
* Relayer is frontrunnable. When relayer submits a transaction someone can see it in tx pool and frontrun it with higher gas price to get the fee and drain relayer funds.
* Workaround: we can set high gas price so that (almost) all fee is used on gas. The relayer will not receive profit this way, but this approach is acceptable until we develop more sophisticated system that prevents frontrunning
* Bugs in contract. Even though we have an extensive experience in smart contract security audits, we can still make mistakes. An external audit is needed to reduce probablility of bugs
* ~~Nullifier griefing. when you submit a withdraw transaction you reveal the nullifier for your note. If someone manages to
make a deposit with the same nullifier and withdraw it while your transaction is still in tx pool, your note will be considered
spent since it has the same nullifier and it will prevent you from withdrawing your funds~~
* Fixed by sending nullifier hash instead of plain nullifier
![image](docs/diagram.png)
## Whitepaper
**[TornadoCash_whitepaper_v1.4.pdf](https://tornado.cash/audits/TornadoCash_whitepaper_v1.4.pdf)**
## Was it audited?
Tornado.cash protocols, circuits, and smart contracts were audited by a group of experts from [ABDK Consulting](https://www.abdk.consulting), specializing in zero-knowledge, cryptography, and smart contracts.
During the audit, no critical issues were found and all outstanding issues were fixed. The results can be found here:
- Cryptographic review https://tornado.cash/audits/TornadoCash_cryptographic_review_ABDK.pdf
- Smart contract audit https://tornado.cash/audits/TornadoCash_contract_audit_ABDK.pdf
- Zk-SNARK circuits audit https://tornado.cash/audits/TornadoCash_circuit_audit_ABDK.pdf
Underlying circomlib dependency is currently being audited, and the team already published most of the fixes for found issues
## Requirements
1. `node v11.15.0`
2. `npm install -g npx`
## Usage
1. `npm i`
You can see example usage in cli.js, it works both in the console and in the browser.
1. `npm install`
1. `cp .env.example .env`
1. `npm run build:circuit` - may take 10 minutes or more
1. `npm run build:contract`
1. `npm run browserify`
1. `npm run build` - this may take 10 minutes or more
1. `npx ganache-cli`
1. `npm run test` - optionally run tests. It may fail for the first time, just run one more time.
1. `npm run migrate:dev`
1. `./cli.js deposit`
1. `./cli.js withdraw <note from previous step> <destination eth address>`
1. `./cli.js balance <destination eth address>`
1. `npm run test` - optionally runs tests. It may fail on the first try, just run it again.
Use browser version on Kovan:
1. `vi .env` - add your Kovan private key to deploy contracts
1. `npm run migrate`
1. `npx http-server` - serve current dir, you can use any other http server
1. `npx http-server` - serve current dir, you can use any other static http server
1. Open `localhost:8080`
Use the command-line version. Works for Ganache, Kovan, and Mainnet:
### Initialization
1. `cp .env.example .env`
1. `npm run download`
1. `npm run build:contract`
### Ganache
1. make sure you complete steps from Initialization
1. `ganache-cli -i 1337`
1. `npm run migrate:dev`
1. `./cli.js test`
1. `./cli.js --help`
### Kovan, Mainnet
1. Please use https://github.com/tornadocash/tornado-cli
Reason: because tornado-core uses websnark `2041cfa5fa0b71cd5cca9022a4eeea4afe28c9f7` commit hash in order to work with local trusted setup. Tornado-cli uses `4c0af6a8b65aabea3c09f377f63c44e7a58afa6d` commit with production trusted setup of tornadoCash
Example:
```bash
./cli.js deposit ETH 0.1 --rpc https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448
```
> Your note: tornado-eth-0.1-42-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652
> Tornado ETH balance is 8.9
> Sender account ETH balance is 1004873.470619891361352542
> Submitting deposit transaction
> Tornado ETH balance is 9
> Sender account ETH balance is 1004873.361652048361352542
```bash
./cli.js withdraw tornado-eth-0.1-42-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 0x8589427373D6D84E98730D7795D8f6f8731FDA16 --rpc https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448 --relayer https://kovan-frelay.duckdns.org
```
> Relay address: 0x6A31736e7490AbE5D5676be059DFf064AB4aC754
> Getting current state from tornado contract
> Generating SNARK proof
> Proof time: 9117.051ms
> Sending withdraw transaction through the relay
> Transaction submitted through the relay. View transaction on etherscan https://kovan.etherscan.io/tx/0xcb21ae8cad723818c6bc7273e83e00c8393fcdbe74802ce5d562acad691a2a7b
> Transaction mined in block 17036120
> Done
## Deploy ETH Tornado Cash
1. `cp .env.example .env`
1. Tune all necessary params
1. `npx truffle migrate --network kovan --reset --f 2 --to 4`
## Deploy ERC20 Tornado Cash
1. `cp .env.example .env`
1. Tune all necessary params
1. `npx truffle migrate --network kovan --reset --f 2 --to 3`
1. `npx truffle migrate --network kovan --reset --f 5`
**Note**. If you want to reuse the same verifier for all the instances, then after you deployed one of the instances you should only run the 4th or 5th migration for ETH or ERC20 contracts respectively (`--f 4 --to 4` or `--f 5`).
## How to resolve ENS name to DNS name for a relayer
1. Visit https://etherscan.io/enslookup and put relayer ENS name to the form.
2. Copy the namehash (1) and click on the `Resolver` link (2)
![enslookup](docs/enslookup.png)
3. Go to the `Contract` tab. Click on `Read Contract` and scroll down to the `5. text` method.
4. Put the values:
![resolver](docs/resolver.png)
5. Click `Query` and you will get the DNS name. Just add `https://` to it and use it as `relayer url`
## Credits
Special thanks to @barryWhiteHat and @kobigurk for valuable input,
and to @jbaylina for awesome [Circom](https://github.com/iden3/circom) & [Websnark](https://github.com/iden3/websnark) framework
and @jbaylina for awesome [Circom](https://github.com/iden3/circom) & [Websnark](https://github.com/iden3/websnark) framework
## Minimal demo example
1. `npm i`
1. `ganache-cli -d`
1. `npm run download`
1. `npm run build:contract`
1. `cp .env.example .env`
1. `npm run migrate:dev`
1. `node minimal-demo.js`
## Run tests/coverage
Prepare test environment:
```
yarn install
yarn download
cp .env.example .env
npx ganache-cli > /dev/null &
npm run migrate:dev
```
Run tests:
```
yarn test
```
Run coverage:
```
yarn coverage
```
## Emulate MPC trusted setup ceremony
```bash
cargo install zkutil
npx circom circuits/withdraw.circom -o build/circuits/withdraw.json
zkutil setup -c build/circuits/withdraw.json -p build/circuits/withdraw.params
zkutil export-keys -c build/circuits/withdraw.json -p build/circuits/withdraw.params -r build/circuits/withdraw_proving_key.json -v build/circuits/withdraw_verification_key.json
zkutil generate-verifier -p build/circuits/withdraw.params -v build/circuits/Verifier.sol
sed -i -e 's/pragma solidity \^0.6.0/pragma solidity 0.5.17/g' ./build/circuits/Verifier.sol
```

View File

@ -1,72 +1,50 @@
include "../node_modules/circomlib/circuits/mimcsponge.circom";
// Computes MiMC(left + right)
template HashLeftRight(rounds) {
// Computes MiMC([left, right])
template HashLeftRight() {
signal input left;
signal input right;
signal output hash;
component hasher = MiMCSponge(2, rounds, 1);
component hasher = MiMCSponge(2, 1);
hasher.ins[0] <== left;
hasher.ins[1] <== right;
hasher.k <== 0;
hash <== hasher.outs[0];
}
// if pathIndex == 0 returns (left = inputElement, right = pathElement)
// if pathIndex == 1 returns (left = pathElement, right = inputElement)
template Selector() {
signal input inputElement;
signal input pathElement;
signal input pathIndex;
// if s == 0 returns [in[0], in[1]]
// if s == 1 returns [in[1], in[0]]
template DualMux() {
signal input in[2];
signal input s;
signal output out[2];
signal output left;
signal output right;
signal leftSelector1;
signal leftSelector2;
signal rightSelector1;
signal rightSelector2;
pathIndex * (1-pathIndex) === 0
leftSelector1 <== (1 - pathIndex) * inputElement;
leftSelector2 <== (pathIndex) * pathElement;
rightSelector1 <== (pathIndex) * inputElement;
rightSelector2 <== (1 - pathIndex) * pathElement;
left <== leftSelector1 + leftSelector2;
right <== rightSelector1 + rightSelector2;
s * (1 - s) === 0
out[0] <== (in[1] - in[0])*s + in[0];
out[1] <== (in[0] - in[1])*s + in[1];
}
// Verifies that merkle proof is correct for given merkle root and a leaf
// pathIndex input is an array of 0/1 selectors telling whether given pathElement is on the left or right side of merkle path
template MerkleTree(levels, rounds) {
// pathIndices input is an array of 0/1 selectors telling whether given pathElement is on the left or right side of merkle path
template MerkleTreeChecker(levels) {
signal input leaf;
signal input root;
signal private input pathElements[levels];
signal private input pathIndex[levels];
signal input pathElements[levels];
signal input pathIndices[levels];
component selectors[levels];
component hashers[levels];
for (var i = 0; i < levels; i++) {
selectors[i] = Selector();
hashers[i] = HashLeftRight(rounds);
selectors[i] = DualMux();
selectors[i].in[0] <== i == 0 ? leaf : hashers[i - 1].hash;
selectors[i].in[1] <== pathElements[i];
selectors[i].s <== pathIndices[i];
selectors[i].pathElement <== pathElements[i];
selectors[i].pathIndex <== pathIndex[i];
hashers[i].left <== selectors[i].left;
hashers[i].right <== selectors[i].right;
}
selectors[0].inputElement <== leaf;
for (var i = 1; i < levels; i++) {
selectors[i].inputElement <== hashers[i-1].hash;
hashers[i] = HashLeftRight();
hashers[i].left <== selectors[i].out[0];
hashers[i].right <== selectors[i].out[1];
}
root === hashers[levels - 1].hash;

View File

@ -4,9 +4,8 @@ include "merkleTree.circom";
// computes Pedersen(nullifier + secret)
template CommitmentHasher() {
signal private input nullifier;
signal private input secret;
signal input nullifier;
signal input secret;
signal output commitment;
signal output nullifierHash;
@ -27,37 +26,42 @@ template CommitmentHasher() {
}
// Verifies that commitment that corresponds to given secret and nullifier is included in the merkle tree of deposits
template Withdraw(levels, rounds) {
template Withdraw(levels) {
signal input root;
signal input nullifierHash;
signal input receiver; // not taking part in any computations
signal input fee; // not taking part in any computations
signal input recipient; // not taking part in any computations
signal input relayer; // not taking part in any computations
signal input fee; // not taking part in any computations
signal input refund; // not taking part in any computations
signal private input nullifier;
signal private input secret;
signal private input pathElements[levels];
signal private input pathIndex[levels];
signal private input pathIndices[levels];
component hasher = CommitmentHasher();
hasher.nullifier <== nullifier;
hasher.secret <== secret;
hasher.nullifierHash === nullifierHash;
nullifierHash === hasher.nullifierHash;
component tree = MerkleTree(levels, rounds);
component tree = MerkleTreeChecker(levels);
tree.leaf <== hasher.commitment;
tree.root <== root;
for (var i = 0; i < levels; i++) {
tree.pathElements[i] <== pathElements[i];
tree.pathIndex[i] <== pathIndex[i];
tree.pathIndices[i] <== pathIndices[i];
}
// Add hidden signals to make sure that tampering with receiver or fee will invalidate the snark proof
// Add hidden signals to make sure that tampering with recipient or fee will invalidate the snark proof
// Most likely it is not required, but it's better to stay on the safe side and it only takes 2 constraints
// Squares are used to prevent optimizer from removing those constraints
signal receiverSquare;
signal recipientSquare;
signal feeSquare;
receiverSquare <== receiver * receiver;
signal relayerSquare;
signal refundSquare;
recipientSquare <== recipient * recipient;
feeSquare <== fee * fee;
relayerSquare <== relayer * relayer;
refundSquare <== refund * refund;
}
component main = Withdraw(16, 220);
component main = Withdraw(20);

200
cli.js
View File

@ -1,200 +0,0 @@
#!/usr/bin/env node
// Temporary demo client
// Works both in browser and node.js
const fs = require('fs')
const assert = require('assert')
const snarkjs = require('snarkjs')
const crypto = require('crypto')
const circomlib = require('circomlib')
const bigInt = snarkjs.bigInt
const merkleTree = require('./lib/MerkleTree')
const Web3 = require('web3')
const buildGroth16 = require('websnark/src/groth16')
const websnarkUtils = require('websnark/src/utils')
let web3, mixer, circuit, proving_key, groth16
let MERKLE_TREE_HEIGHT, AMOUNT, EMPTY_ELEMENT
const inBrowser = (typeof window !== 'undefined')
const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
function createDeposit(nullifier, secret) {
let deposit = { nullifier, secret }
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
deposit.commitment = pedersenHash(deposit.preimage)
return deposit
}
async function deposit() {
const deposit = createDeposit(rbigint(31), rbigint(31))
console.log('Submitting deposit transaction')
await mixer.methods.deposit('0x' + deposit.commitment.toString(16)).send({ value: AMOUNT, from: (await web3.eth.getAccounts())[0], gas:1e6 })
const note = '0x' + deposit.preimage.toString('hex')
console.log('Your note:', note)
return note
}
async function getBalance(receiver) {
const balance = await web3.eth.getBalance(receiver)
console.log('Balance is ', web3.utils.fromWei(balance))
}
async function withdraw(note, receiver) {
let buf = Buffer.from(note.slice(2), 'hex')
let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62)))
console.log('Getting current state from mixer contract')
const events = await mixer.getPastEvents('Deposit', { fromBlock: mixer.deployedBlock, toBlock: 'latest' })
let leafIndex
const commitment = deposit.commitment.toString(16).padStart('66', '0x000000')
const leaves = events
.sort((a, b) => a.returnValues.leafIndex.sub(b.returnValues.leafIndex))
.map(e => {
if (e.returnValues.commitment.eq(commitment)) {
leafIndex = e.returnValues.leafIndex.toNumber()
}
return e.returnValues.commitment
})
const tree = new merkleTree(MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, leaves)
const validRoot = await mixer.methods.isKnownRoot(await tree.root()).call()
const nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
const nullifierHashToCheck = nullifierHash.toString(16).padStart('66', '0x000000')
const isSpent = await mixer.methods.isSpent(nullifierHashToCheck).call()
assert(validRoot === true)
assert(isSpent === false)
assert(leafIndex >= 0)
const { root, path_elements, path_index } = await tree.path(leafIndex)
// Circuit input
const input = {
// public
root: root,
nullifierHash,
receiver: bigInt(receiver),
fee: bigInt(0),
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndex: path_index,
}
console.log('Generating SNARK proof')
console.time('Proof time')
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
console.timeEnd('Proof time')
console.log('Submitting withdraw transaction')
await mixer.methods.withdraw(pi_a, pi_b, pi_c, publicSignals).send({ from: (await web3.eth.getAccounts())[0], gas: 1e6 })
console.log('Done')
}
async function init() {
let contractJson
if (inBrowser) {
web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 })
contractJson = await (await fetch('build/contracts/Mixer.json')).json()
circuit = await (await fetch('build/circuits/withdraw.json')).json()
proving_key = await (await fetch('build/circuits/withdraw_proving_key.bin')).arrayBuffer()
MERKLE_TREE_HEIGHT = 16
AMOUNT = 1e18
EMPTY_ELEMENT = 0
} else {
web3 = new Web3('http://localhost:8545', null, { transactionConfirmationBlocks: 1 })
contractJson = require('./build/contracts/Mixer.json')
circuit = require('./build/circuits/withdraw.json')
proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
require('dotenv').config()
MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT
AMOUNT = process.env.AMOUNT
EMPTY_ELEMENT = process.env.EMPTY_ELEMENT
}
groth16 = await buildGroth16()
let netId = await web3.eth.net.getId()
const tx = await web3.eth.getTransaction(contractJson.networks[netId].transactionHash)
mixer = new web3.eth.Contract(contractJson.abi, contractJson.networks[netId].address)
mixer.deployedBlock = tx.blockNumber
console.log('Loaded')
}
// ========== CLI related stuff below ==============
function printHelp(code = 0) {
console.log(`Usage:
Submit a deposit from default eth account and return the resulting note
$ ./cli.js deposit
Withdraw a note to 'receiver' account
$ ./cli.js withdraw <note> <receiver>
Check address balance
$ ./cli.js balance <address>
Example:
$ ./cli.js deposit
...
Your note: 0x1941fa999e2b4bfeec3ce53c2440c3bc991b1b84c9bb650ea19f8331baf621001e696487e2a2ee54541fa12f49498d71e24d00b1731a8ccd4f5f5126f3d9f400
$ ./cli.js withdraw 0x1941fa999e2b4bfeec3ce53c2440c3bc991b1b84c9bb650ea19f8331baf621001e696487e2a2ee54541fa12f49498d71e24d00b1731a8ccd4f5f5126f3d9f400 0xee6249BA80596A4890D1BD84dbf5E4322eA4E7f0
`)
process.exit(code)
}
if (inBrowser) {
window.deposit = deposit
window.withdraw = async () => {
const note = prompt('Enter the note to withdraw')
const receiver = (await web3.eth.getAccounts())[0]
await withdraw(note, receiver)
}
init()
} else {
const args = process.argv.slice(2)
if (args.length === 0) {
printHelp()
} else {
switch (args[0]) {
case 'deposit':
if (args.length === 1) {
init().then(() => deposit()).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
}
else
printHelp(1)
break
case 'balance':
if (args.length === 2 && /^0x[0-9a-fA-F]{40}$/.test(args[1])) {
init().then(() => getBalance(args[1])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
} else
printHelp(1)
break
case 'withdraw':
if (args.length === 3 && /^0x[0-9a-fA-F]{124}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) {
init().then(() => withdraw(args[1], args[2])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
}
else
printHelp(1)
break
case 'auto':
if (args.length === 1) {
(async () => {
await init()
const note = await deposit()
await withdraw(note, (await web3.eth.getAccounts())[0])
process.exit(0)
})()
}
else
printHelp(1)
break
default:
printHelp(1)
}
}
}

View File

@ -0,0 +1,59 @@
// https://tornado.cash
/*
* d888888P dP a88888b. dP
* 88 88 d8' `88 88
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
*/
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
import "./Tornado.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
contract ERC20Tornado is Tornado {
using SafeERC20 for IERC20;
IERC20 public token;
constructor(
IVerifier _verifier,
IHasher _hasher,
uint256 _denomination,
uint32 _merkleTreeHeight,
IERC20 _token
) Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight) {
token = _token;
}
function _processDeposit() internal override {
require(msg.value == 0, "ETH value is supposed to be 0 for ERC20 instance");
token.safeTransferFrom(msg.sender, address(this), denomination);
}
function _processWithdraw(
address payable _recipient,
address payable _relayer,
uint256 _fee,
uint256 _refund
) internal override {
require(msg.value == _refund, "Incorrect refund amount received by the contract");
token.safeTransfer(_recipient, denomination - _fee);
if (_fee > 0) {
token.safeTransfer(_relayer, _fee);
}
if (_refund > 0) {
(bool success, ) = _recipient.call{ value: _refund }("");
if (!success) {
// let's return _refund back to the relayer
_relayer.transfer(_refund);
}
}
}
}

46
contracts/ETHTornado.sol Normal file
View File

@ -0,0 +1,46 @@
// https://tornado.cash
/*
* d888888P dP a88888b. dP
* 88 88 d8' `88 88
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
*/
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
import "./Tornado.sol";
contract ETHTornado is Tornado {
constructor(
IVerifier _verifier,
IHasher _hasher,
uint256 _denomination,
uint32 _merkleTreeHeight
) Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight) {}
function _processDeposit() internal override {
require(msg.value == denomination, "Please send `mixDenomination` ETH along with transaction");
}
function _processWithdraw(
address payable _recipient,
address payable _relayer,
uint256 _fee,
uint256 _refund
) internal override {
// sanity checks
require(msg.value == 0, "Message value is supposed to be zero for ETH instance");
require(_refund == 0, "Refund value is supposed to be zero for ETH instance");
(bool success, ) = _recipient.call{ value: denomination - _fee }("");
require(success, "payment to _recipient did not go thru");
if (_fee > 0) {
(success, ) = _relayer.call{ value: _fee }("");
require(success, "payment to _relayer did not go thru");
}
}
}

View File

@ -1,137 +1,160 @@
// https://tornado.cash
/*
* d888888P dP a88888b. dP
* 88 88 d8' `88 88
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
*/
* d888888P dP a88888b. dP
* 88 88 d8' `88 88
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
*/
pragma solidity ^0.5.8;
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
library MiMC {
function MiMCSponge(uint256 in_xL, uint256 in_xR, uint256 in_k) public pure returns (uint256 xL, uint256 xR);
interface IHasher {
function MiMCSponge(uint256 in_xL, uint256 in_xR) external pure returns (uint256 xL, uint256 xR);
}
contract MerkleTreeWithHistory {
uint256 public levels;
uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
uint256 public constant ZERO_VALUE = 21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE
IHasher public immutable hasher;
uint8 constant ROOT_HISTORY_SIZE = 100;
uint256[] private _roots;
uint256 public current_root = 0;
uint32 public levels;
uint256[] private _filled_subtrees;
uint256[] private _zeros;
// the following variables are made public for easier testing and debugging and
// are not supposed to be accessed in regular code
uint32 public next_index = 0;
// filledSubtrees and roots could be bytes32[size], but using mappings makes it cheaper because
// it removes index range check on every interaction
mapping(uint256 => bytes32) public filledSubtrees;
mapping(uint256 => bytes32) public roots;
uint32 public constant ROOT_HISTORY_SIZE = 30;
uint32 public currentRootIndex = 0;
uint32 public nextIndex = 0;
constructor(uint256 tree_levels, uint256 zero_value) public {
levels = tree_levels;
constructor(uint32 _levels, IHasher _hasher) {
require(_levels > 0, "_levels should be greater than zero");
require(_levels < 32, "_levels should be less than 32");
levels = _levels;
hasher = _hasher;
_zeros.push(zero_value);
_filled_subtrees.push(_zeros[0]);
for (uint8 i = 1; i < levels; i++) {
_zeros.push(hashLeftRight(_zeros[i-1], _zeros[i-1]));
_filled_subtrees.push(_zeros[i]);
for (uint32 i = 0; i < _levels; i++) {
filledSubtrees[i] = zeros(i);
}
_roots = new uint256[](ROOT_HISTORY_SIZE);
_roots[0] = hashLeftRight(_zeros[levels - 1], _zeros[levels - 1]);
roots[0] = zeros(_levels - 1);
}
function hashLeftRight(uint256 left, uint256 right) public pure returns (uint256 mimc_hash) {
uint256 k = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
uint256 R = 0;
/**
@dev Hash 2 tree leaves, returns MiMC(_left, _right)
*/
function hashLeftRight(
IHasher _hasher,
bytes32 _left,
bytes32 _right
) public pure returns (bytes32) {
require(uint256(_left) < FIELD_SIZE, "_left should be inside the field");
require(uint256(_right) < FIELD_SIZE, "_right should be inside the field");
uint256 R = uint256(_left);
uint256 C = 0;
R = addmod(R, left, k);
(R, C) = MiMC.MiMCSponge(R, C, 0);
R = addmod(R, right, k);
(R, C) = MiMC.MiMCSponge(R, C, 0);
mimc_hash = R;
(R, C) = _hasher.MiMCSponge(R, C);
R = addmod(R, uint256(_right), FIELD_SIZE);
(R, C) = _hasher.MiMCSponge(R, C);
return bytes32(R);
}
function _insert(uint256 leaf) internal {
uint32 current_index = next_index;
require(current_index != 2**(levels - 1), "Merkle tree is full");
next_index += 1;
uint256 current_level_hash = leaf;
uint256 left;
uint256 right;
function _insert(bytes32 _leaf) internal returns (uint32 index) {
uint32 _nextIndex = nextIndex;
require(_nextIndex != uint32(2)**levels, "Merkle tree is full. No more leaves can be added");
uint32 currentIndex = _nextIndex;
bytes32 currentLevelHash = _leaf;
bytes32 left;
bytes32 right;
for (uint256 i = 0; i < levels; i++) {
if (current_index % 2 == 0) {
left = current_level_hash;
right = _zeros[i];
_filled_subtrees[i] = current_level_hash;
for (uint32 i = 0; i < levels; i++) {
if (currentIndex % 2 == 0) {
left = currentLevelHash;
right = zeros(i);
filledSubtrees[i] = currentLevelHash;
} else {
left = _filled_subtrees[i];
right = current_level_hash;
left = filledSubtrees[i];
right = currentLevelHash;
}
current_level_hash = hashLeftRight(left, right);
current_index /= 2;
currentLevelHash = hashLeftRight(hasher, left, right);
currentIndex /= 2;
}
current_root = (current_root + 1) % ROOT_HISTORY_SIZE;
_roots[current_root] = current_level_hash;
uint32 newRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
currentRootIndex = newRootIndex;
roots[newRootIndex] = currentLevelHash;
nextIndex = _nextIndex + 1;
return _nextIndex;
}
function isKnownRoot(uint256 root) public view returns(bool) {
if (root == 0) {
/**
@dev Whether the root is present in the root history
*/
function isKnownRoot(bytes32 _root) public view returns (bool) {
if (_root == 0) {
return false;
}
// search most recent first
uint256 i;
for(i = current_root; i < 2**256 - 1; i--) {
if (root == _roots[i]) {
uint32 _currentRootIndex = currentRootIndex;
uint32 i = _currentRootIndex;
do {
if (_root == roots[i]) {
return true;
}
}
// process the rest of roots
for(i = ROOT_HISTORY_SIZE - 1; i > current_root; i--) {
if (root == _roots[i]) {
return true;
if (i == 0) {
i = ROOT_HISTORY_SIZE;
}
}
i--;
} while (i != _currentRootIndex);
return false;
// or we can do that in other way
// uint256 i = _current_root;
// do {
// if (root == _roots[i]) {
// return true;
// }
// if (i == 0) {
// i = ROOT_HISTORY_SIZE;
// }
// i--;
// } while (i != _current_root);
}
function getLastRoot() public view returns(uint256) {
return _roots[current_root];
/**
@dev Returns the last root
*/
function getLastRoot() public view returns (bytes32) {
return roots[currentRootIndex];
}
function roots() public view returns(uint256[] memory) {
return _roots;
}
function filled_subtrees() public view returns(uint256[] memory) {
return _filled_subtrees;
}
function zeros() public view returns(uint256[] memory) {
return _zeros;
/// @dev provides Zero (Empty) elements for a MiMC MerkleTree. Up to 32 levels
function zeros(uint256 i) public pure returns (bytes32) {
if (i == 0) return bytes32(0x2fe54c60d3acabf3343a35b6eba15db4821b340f76e741e2249685ed4899af6c);
else if (i == 1) return bytes32(0x256a6135777eee2fd26f54b8b7037a25439d5235caee224154186d2b8a52e31d);
else if (i == 2) return bytes32(0x1151949895e82ab19924de92c40a3d6f7bcb60d92b00504b8199613683f0c200);
else if (i == 3) return bytes32(0x20121ee811489ff8d61f09fb89e313f14959a0f28bb428a20dba6b0b068b3bdb);
else if (i == 4) return bytes32(0x0a89ca6ffa14cc462cfedb842c30ed221a50a3d6bf022a6a57dc82ab24c157c9);
else if (i == 5) return bytes32(0x24ca05c2b5cd42e890d6be94c68d0689f4f21c9cec9c0f13fe41d566dfb54959);
else if (i == 6) return bytes32(0x1ccb97c932565a92c60156bdba2d08f3bf1377464e025cee765679e604a7315c);
else if (i == 7) return bytes32(0x19156fbd7d1a8bf5cba8909367de1b624534ebab4f0f79e003bccdd1b182bdb4);
else if (i == 8) return bytes32(0x261af8c1f0912e465744641409f622d466c3920ac6e5ff37e36604cb11dfff80);
else if (i == 9) return bytes32(0x0058459724ff6ca5a1652fcbc3e82b93895cf08e975b19beab3f54c217d1c007);
else if (i == 10) return bytes32(0x1f04ef20dee48d39984d8eabe768a70eafa6310ad20849d4573c3c40c2ad1e30);
else if (i == 11) return bytes32(0x1bea3dec5dab51567ce7e200a30f7ba6d4276aeaa53e2686f962a46c66d511e5);
else if (i == 12) return bytes32(0x0ee0f941e2da4b9e31c3ca97a40d8fa9ce68d97c084177071b3cb46cd3372f0f);
else if (i == 13) return bytes32(0x1ca9503e8935884501bbaf20be14eb4c46b89772c97b96e3b2ebf3a36a948bbd);
else if (i == 14) return bytes32(0x133a80e30697cd55d8f7d4b0965b7be24057ba5dc3da898ee2187232446cb108);
else if (i == 15) return bytes32(0x13e6d8fc88839ed76e182c2a779af5b2c0da9dd18c90427a644f7e148a6253b6);
else if (i == 16) return bytes32(0x1eb16b057a477f4bc8f572ea6bee39561098f78f15bfb3699dcbb7bd8db61854);
else if (i == 17) return bytes32(0x0da2cb16a1ceaabf1c16b838f7a9e3f2a3a3088d9e0a6debaa748114620696ea);
else if (i == 18) return bytes32(0x24a3b3d822420b14b5d8cb6c28a574f01e98ea9e940551d2ebd75cee12649f9d);
else if (i == 19) return bytes32(0x198622acbd783d1b0d9064105b1fc8e4d8889de95c4c519b3f635809fe6afc05);
else if (i == 20) return bytes32(0x29d7ed391256ccc3ea596c86e933b89ff339d25ea8ddced975ae2fe30b5296d4);
else if (i == 21) return bytes32(0x19be59f2f0413ce78c0c3703a3a5451b1d7f39629fa33abd11548a76065b2967);
else if (i == 22) return bytes32(0x1ff3f61797e538b70e619310d33f2a063e7eb59104e112e95738da1254dc3453);
else if (i == 23) return bytes32(0x10c16ae9959cf8358980d9dd9616e48228737310a10e2b6b731c1a548f036c48);
else if (i == 24) return bytes32(0x0ba433a63174a90ac20992e75e3095496812b652685b5e1a2eae0b1bf4e8fcd1);
else if (i == 25) return bytes32(0x019ddb9df2bc98d987d0dfeca9d2b643deafab8f7036562e627c3667266a044c);
else if (i == 26) return bytes32(0x2d3c88b23175c5a5565db928414c66d1912b11acf974b2e644caaac04739ce99);
else if (i == 27) return bytes32(0x2eab55f6ae4e66e32c5189eed5c470840863445760f5ed7e7b69b2a62600f354);
else if (i == 28) return bytes32(0x002df37a2642621802383cf952bf4dd1f32e05433beeb1fd41031fb7eace979d);
else if (i == 29) return bytes32(0x104aeb41435db66c3e62feccc1d6f5d98d0a0ed75d1374db457cf462e3a1f427);
else if (i == 30) return bytes32(0x1f3c6fd858e9a7d4b0d1f38e256a09d81d5a5e3c963987e2d4b814cfab7c6ebb);
else if (i == 31) return bytes32(0x2c7a07d20dff79d01fecedc1134284a8d08436606c93693b67e333f671bf69cc);
else revert("Index out of bounds");
}
}

View File

@ -1,23 +0,0 @@
pragma solidity >=0.4.21 <0.6.0;
contract Migrations {
address public owner;
uint public last_completed_migration;
constructor() public {
owner = msg.sender;
}
modifier restricted() {
if (msg.sender == owner) _;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}

View File

@ -1,104 +0,0 @@
// https://tornado.cash
/*
* d888888P dP a88888b. dP
* 88 88 d8' `88 88
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
*/
pragma solidity ^0.5.8;
import "./MerkleTreeWithHistory.sol";
contract IVerifier {
function verifyProof(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[4] memory input) public returns(bool);
}
contract Mixer is MerkleTreeWithHistory {
uint256 public transferValue;
bool public isDepositsEnabled = true;
// operator can disable new deposits in case of emergency
// it also receives a relayer fee
address payable public operator;
mapping(uint256 => bool) public nullifierHashes;
// we store all commitments just to prevent accidental deposits with the same commitment
mapping(uint256 => bool) public commitments;
IVerifier public verifier;
event Deposit(uint256 indexed commitment, uint256 leafIndex, uint256 timestamp);
event Withdraw(address to, uint256 nullifierHash, uint256 fee);
/**
@dev The constructor
@param _verifier the address of SNARK verifier for this contract
@param _transferValue the value for all deposits in this contract in wei
*/
constructor(
address _verifier,
uint256 _transferValue,
uint8 _merkleTreeHeight,
uint256 _emptyElement,
address payable _operator
) MerkleTreeWithHistory(_merkleTreeHeight, _emptyElement) public {
verifier = IVerifier(_verifier);
transferValue = _transferValue;
operator = _operator;
}
/**
@dev Deposit funds into mixer. The caller must send value equal to `transferValue` of this mixer.
@param commitment the note commitment, which is PedersenHash(nullifier + secret)
*/
function deposit(uint256 commitment) public payable {
require(isDepositsEnabled, "deposits disabled");
require(msg.value == transferValue, "Please send `transferValue` ETH along with transaction");
require(!commitments[commitment], "The commitment has been submitted");
_insert(commitment);
commitments[commitment] = true;
emit Deposit(commitment, next_index - 1, block.timestamp);
}
/**
@dev Withdraw deposit from the mixer. `a`, `b`, and `c` are zkSNARK proof data, and input is an array of circuit public inputs
`input` array consists of:
- merkle root of all deposits in the mixer
- hash of unique deposit nullifier to prevent double spends
- the receiver of funds
- optional fee that goes to the transaction sender (usually a relay)
*/
function withdraw(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[4] memory input) public {
uint256 root = input[0];
uint256 nullifierHash = input[1];
address payable receiver = address(input[2]);
uint256 fee = input[3];
require(!nullifierHashes[nullifierHash], "The note has been already spent");
require(fee < transferValue, "Fee exceeds transfer value");
require(isKnownRoot(root), "Cannot find your merkle root"); // Make sure to use a recent one
require(verifier.verifyProof(a, b, c, input), "Invalid withdraw proof");
nullifierHashes[nullifierHash] = true;
receiver.transfer(transferValue - fee);
if (fee > 0) {
operator.transfer(fee);
}
emit Withdraw(receiver, nullifierHash, fee);
}
function toggleDeposits() external {
require(msg.sender == operator, "unauthorized");
isDepositsEnabled = !isDepositsEnabled;
}
function changeOperator(address payable _newAccount) external {
require(msg.sender == operator, "unauthorized");
operator = _newAccount;
}
function isSpent(uint256 nullifier) public view returns(bool) {
return nullifierHashes[nullifier];
}
}

View File

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

View File

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

View File

@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
interface IDeployer {
function deploy(bytes memory _initCode, bytes32 _salt) external returns (address payable createdContract);
}

33
contracts/Mocks/IUSDT.sol Normal file
View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
interface ERC20Basic {
function _totalSupply() external returns (uint256);
function totalSupply() external view returns (uint256);
function balanceOf(address who) external view returns (uint256);
function transfer(address to, uint256 value) external;
event Transfer(address indexed from, address indexed to, uint256 value);
}
/**
* @title ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
interface IUSDT is ERC20Basic {
function allowance(address owner, address spender) external view returns (uint256);
function transferFrom(
address from,
address to,
uint256 value
) external;
function approve(address spender, uint256 value) external;
event Approval(address indexed owner, address indexed spender, uint256 value);
}

View File

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

123
contracts/Tornado.sol Normal file
View File

@ -0,0 +1,123 @@
// https://tornado.cash
/*
* d888888P dP a88888b. dP
* 88 88 d8' `88 88
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
*/
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
import "./MerkleTreeWithHistory.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
interface IVerifier {
function verifyProof(bytes memory _proof, uint256[6] memory _input) external returns (bool);
}
abstract contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
IVerifier public immutable verifier;
uint256 public denomination;
mapping(bytes32 => bool) public nullifierHashes;
// we store all commitments just to prevent accidental deposits with the same commitment
mapping(bytes32 => bool) public commitments;
event Deposit(bytes32 indexed commitment, uint32 leafIndex, uint256 timestamp);
event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 fee);
/**
@dev The constructor
@param _verifier the address of SNARK verifier for this contract
@param _hasher the address of MiMC hash contract
@param _denomination transfer amount for each deposit
@param _merkleTreeHeight the height of deposits' Merkle Tree
*/
constructor(
IVerifier _verifier,
IHasher _hasher,
uint256 _denomination,
uint32 _merkleTreeHeight
) MerkleTreeWithHistory(_merkleTreeHeight, _hasher) {
require(_denomination > 0, "denomination should be greater than 0");
verifier = _verifier;
denomination = _denomination;
}
/**
@dev Deposit funds into the contract. The caller must send (for ETH) or approve (for ERC20) value equal to or `denomination` of this instance.
@param _commitment the note commitment, which is PedersenHash(nullifier + secret)
*/
function deposit(bytes32 _commitment) external payable nonReentrant {
require(!commitments[_commitment], "The commitment has been submitted");
uint32 insertedIndex = _insert(_commitment);
commitments[_commitment] = true;
_processDeposit();
emit Deposit(_commitment, insertedIndex, block.timestamp);
}
/** @dev this function is defined in a child contract */
function _processDeposit() internal virtual;
/**
@dev Withdraw a deposit from the contract. `proof` is a zkSNARK proof data, and input is an array of circuit public inputs
`input` array consists of:
- merkle root of all deposits in the contract
- hash of unique deposit nullifier to prevent double spends
- the recipient of funds
- optional fee that goes to the transaction sender (usually a relay)
*/
function withdraw(
bytes calldata _proof,
bytes32 _root,
bytes32 _nullifierHash,
address payable _recipient,
address payable _relayer,
uint256 _fee,
uint256 _refund
) external payable nonReentrant {
require(_fee <= denomination, "Fee exceeds transfer value");
require(!nullifierHashes[_nullifierHash], "The note has been already spent");
require(isKnownRoot(_root), "Cannot find your merkle root"); // Make sure to use a recent one
require(
verifier.verifyProof(
_proof,
[uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund]
),
"Invalid withdraw proof"
);
nullifierHashes[_nullifierHash] = true;
_processWithdraw(_recipient, _relayer, _fee, _refund);
emit Withdrawal(_recipient, _nullifierHash, _relayer, _fee);
}
/** @dev this function is defined in a child contract */
function _processWithdraw(
address payable _recipient,
address payable _relayer,
uint256 _fee,
uint256 _refund
) internal virtual;
/** @dev whether a note is already spent */
function isSpent(bytes32 _nullifierHash) public view returns (bool) {
return nullifierHashes[_nullifierHash];
}
/** @dev whether an array of notes is already spent */
function isSpentArray(bytes32[] calldata _nullifierHashes) external view returns (bool[] memory spent) {
spent = new bool[](_nullifierHashes.length);
for (uint256 i = 0; i < _nullifierHashes.length; i++) {
if (isSpent(_nullifierHashes[i])) {
spent[i] = true;
}
}
}
}

View File

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

231
contracts/Verifier.sol Normal file
View File

@ -0,0 +1,231 @@
/**
*Submitted for verification at Etherscan.io on 2020-05-12
*/
// https://tornado.cash Verifier.sol generated by trusted setup ceremony.
/*
* d888888P dP a88888b. dP
* 88 88 d8' `88 88
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
*/
// SPDX-License-Identifier: MIT
// Copyright 2017 Christian Reitwiessner
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// 2019 OKIMS
pragma solidity ^0.7.0;
library Pairing {
uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
struct G1Point {
uint256 X;
uint256 Y;
}
// Encoding of field elements is: X[0] * z + X[1]
struct G2Point {
uint256[2] X;
uint256[2] Y;
}
/*
* @return The negation of p, i.e. p.plus(p.negate()) should be zero.
*/
function negate(G1Point memory p) internal pure returns (G1Point memory) {
// The prime q in the base field F_q for G1
if (p.X == 0 && p.Y == 0) {
return G1Point(0, 0);
} else {
return G1Point(p.X, PRIME_Q - (p.Y % PRIME_Q));
}
}
/*
* @return r the sum of two points of G1
*/
function plus(
G1Point memory p1,
G1Point memory p2
) internal view returns (G1Point memory r) {
uint256[4] memory input;
input[0] = p1.X;
input[1] = p1.Y;
input[2] = p2.X;
input[3] = p2.Y;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(sub(gas(), 2000), 6, input, 0xc0, r, 0x60)
// Use "invalid" to make gas estimation work
switch success case 0 { invalid() }
}
require(success, "pairing-add-failed");
}
/*
* @return r the product of a point on G1 and a scalar, i.e.
* p == p.scalar_mul(1) and p.plus(p) == p.scalar_mul(2) for all
* points p.
*/
function scalar_mul(G1Point memory p, uint256 s) internal view returns (G1Point memory r) {
uint256[3] memory input;
input[0] = p.X;
input[1] = p.Y;
input[2] = s;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60)
// Use "invalid" to make gas estimation work
switch success case 0 { invalid() }
}
require(success, "pairing-mul-failed");
}
/* @return The result of computing the pairing check
* e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1
* For example,
* pairing([P1(), P1().negate()], [P2(), P2()]) should return true.
*/
function pairing(
G1Point memory a1,
G2Point memory a2,
G1Point memory b1,
G2Point memory b2,
G1Point memory c1,
G2Point memory c2,
G1Point memory d1,
G2Point memory d2
) internal view returns (bool) {
G1Point[4] memory p1 = [a1, b1, c1, d1];
G2Point[4] memory p2 = [a2, b2, c2, d2];
uint256 inputSize = 24;
uint256[] memory input = new uint256[](inputSize);
for (uint256 i = 0; i < 4; i++) {
uint256 j = i * 6;
input[j + 0] = p1[i].X;
input[j + 1] = p1[i].Y;
input[j + 2] = p2[i].X[0];
input[j + 3] = p2[i].X[1];
input[j + 4] = p2[i].Y[0];
input[j + 5] = p2[i].Y[1];
}
uint256[1] memory out;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(sub(gas(), 2000), 8, add(input, 0x20), mul(inputSize, 0x20), out, 0x20)
// Use "invalid" to make gas estimation work
switch success case 0 { invalid() }
}
require(success, "pairing-opcode-failed");
return out[0] != 0;
}
}
contract Verifier {
uint256 constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
using Pairing for *;
struct VerifyingKey {
Pairing.G1Point alfa1;
Pairing.G2Point beta2;
Pairing.G2Point gamma2;
Pairing.G2Point delta2;
Pairing.G1Point[7] IC;
}
struct Proof {
Pairing.G1Point A;
Pairing.G2Point B;
Pairing.G1Point C;
}
function verifyingKey() internal pure returns (VerifyingKey memory vk) {
vk.alfa1 = Pairing.G1Point(uint256(20692898189092739278193869274495556617788530808486270118371701516666252877969), uint256(11713062878292653967971378194351968039596396853904572879488166084231740557279));
vk.beta2 = Pairing.G2Point([uint256(12168528810181263706895252315640534818222943348193302139358377162645029937006), uint256(281120578337195720357474965979947690431622127986816839208576358024608803542)], [uint256(16129176515713072042442734839012966563817890688785805090011011570989315559913), uint256(9011703453772030375124466642203641636825223906145908770308724549646909480510)]);
vk.gamma2 = Pairing.G2Point([uint256(11559732032986387107991004021392285783925812861821192530917403151452391805634), uint256(10857046999023057135944570762232829481370756359578518086990519993285655852781)], [uint256(4082367875863433681332203403145435568316851327593401208105741076214120093531), uint256(8495653923123431417604973247489272438418190587263600148770280649306958101930)]);
vk.delta2 = Pairing.G2Point([uint256(21280594949518992153305586783242820682644996932183186320680800072133486887432), uint256(150879136433974552800030963899771162647715069685890547489132178314736470662)], [uint256(1081836006956609894549771334721413187913047383331561601606260283167615953295), uint256(11434086686358152335540554643130007307617078324975981257823476472104616196090)]);
vk.IC[0] = Pairing.G1Point(uint256(16225148364316337376768119297456868908427925829817748684139175309620217098814), uint256(5167268689450204162046084442581051565997733233062478317813755636162413164690));
vk.IC[1] = Pairing.G1Point(uint256(12882377842072682264979317445365303375159828272423495088911985689463022094260), uint256(19488215856665173565526758360510125932214252767275816329232454875804474844786));
vk.IC[2] = Pairing.G1Point(uint256(13083492661683431044045992285476184182144099829507350352128615182516530014777), uint256(602051281796153692392523702676782023472744522032670801091617246498551238913));
vk.IC[3] = Pairing.G1Point(uint256(9732465972180335629969421513785602934706096902316483580882842789662669212890), uint256(2776526698606888434074200384264824461688198384989521091253289776235602495678));
vk.IC[4] = Pairing.G1Point(uint256(8586364274534577154894611080234048648883781955345622578531233113180532234842), uint256(21276134929883121123323359450658320820075698490666870487450985603988214349407));
vk.IC[5] = Pairing.G1Point(uint256(4910628533171597675018724709631788948355422829499855033965018665300386637884), uint256(20532468890024084510431799098097081600480376127870299142189696620752500664302));
vk.IC[6] = Pairing.G1Point(uint256(15335858102289947642505450692012116222827233918185150176888641903531542034017), uint256(5311597067667671581646709998171703828965875677637292315055030353779531404812));
}
/*
* @returns Whether the proof is valid given the hardcoded verifying key
* above and the public inputs
*/
function verifyProof(
bytes memory proof,
uint256[6] memory input
) public view returns (bool) {
uint256[8] memory p = abi.decode(proof, (uint256[8]));
// Make sure that each element in the proof is less than the prime q
for (uint8 i = 0; i < p.length; i++) {
require(p[i] < PRIME_Q, "verifier-proof-element-gte-prime-q");
}
Proof memory _proof;
_proof.A = Pairing.G1Point(p[0], p[1]);
_proof.B = Pairing.G2Point([p[2], p[3]], [p[4], p[5]]);
_proof.C = Pairing.G1Point(p[6], p[7]);
VerifyingKey memory vk = verifyingKey();
// Compute the linear combination vk_x
Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0);
vk_x = Pairing.plus(vk_x, vk.IC[0]);
// Make sure that every input is less than the snark scalar field
for (uint256 i = 0; i < input.length; i++) {
require(input[i] < SNARK_SCALAR_FIELD, "verifier-gte-snark-scalar-field");
vk_x = Pairing.plus(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i]));
}
return Pairing.pairing(
Pairing.negate(_proof.A),
_proof.B,
vk.alfa1,
vk.beta2,
vk_x,
vk.gamma2,
_proof.C,
vk.delta2
);
}
}

39
contracts/cTornado.sol Normal file
View File

@ -0,0 +1,39 @@
// https://tornado.cash
/*
* d888888P dP a88888b. dP
* 88 88 d8' `88 88
* 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b.
* 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88
* 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88
* dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP
* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
*/
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
import "./ERC20Tornado.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract cTornado is ERC20Tornado {
address public immutable governance = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
IERC20 public immutable comp;
constructor(
IERC20 _comp,
IVerifier _verifier,
IHasher _hasher,
uint256 _denomination,
uint32 _merkleTreeHeight,
IERC20 _token
) ERC20Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight, _token) {
require(address(_comp) != address(0), "Invalid COMP token address");
comp = _comp;
}
/// @dev Moves earned yield of the COMP token to the tornado governance contract
/// To make it work you might need to call `comptroller.claimComp(cPoolAddress)` first
function claimComp() external {
comp.transfer(governance, comp.balanceOf(address(this)));
}
}

25
deploy.js Normal file
View File

@ -0,0 +1,25 @@
const eth = true
const poolSize = '1000000000000000000'
const hasherAddress = '0x83584f83f26aF4eDDA9CBe8C730bc87C364b28fe'
const verifierAddress = '0xce172ce1F20EC0B3728c9965470eaf994A03557A'
const deployerAddress = '0xCEe71753C9820f063b38FDbE4cFDAf1d3D928A80'
const deploySalt = '0x0000000000000000000000000000000000000000000000000000000047941987'
const rpcUrl = 'https://mainnet.infura.io'
const Web3 = require('web3')
const web3 = new Web3(rpcUrl)
const contractData = require('./build/contracts/' + (eth ? 'ETHTornado.json' : 'ERC20Tornado.json'))
const contract = new web3.eth.Contract(contractData.abi)
const bytes = contract
.deploy({
data: contractData.bytecode,
arguments: [verifierAddress, hasherAddress, poolSize, 20],
})
.encodeABI()
console.log('Deploy bytecode', bytes)
const deployer = new web3.eth.Contract(require('./build/contracts/IDeployer.json').abi, deployerAddress)
const receipt = deployer.methods.deploy(bytes, deploySalt)
receipt.then(console.log).catch(console.log)

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

BIN
docs/enslookup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

BIN
docs/resolver.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -1,16 +1,17 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Snark mixer test</title>
</head>
<body>
<p>
Open dev console!<br>
Make sure your Metamask is unlocked and connected to Kovan (or other network you've deployed your contract to)<br>
<a href="#" onclick="deposit()">Deposit</a>
<a href="#" onclick="withdraw()">Withdraw</a>
</p>
<script src="index.js"></script>
</body>
<head>
<meta charset="utf-8" />
<title>Tornado test</title>
</head>
<body>
<p>
Open dev console!<br />
Make sure your Metamask is unlocked and connected to Kovan (or other network you've deployed your
contract to)<br />
<a href="#" onclick="deposit()">Deposit</a>
<a href="#" onclick="withdraw()">Withdraw</a>
</p>
<script src="index.js"></script>
</body>
</html>

View File

@ -1,194 +0,0 @@
const jsStorage = require('./Storage')
const mimcHasher = require('./MiMC')
class MerkleTree {
constructor(n_levels, zero_value, defaultElements, prefix, storage, hasher) {
this.prefix = prefix
this.storage = storage || new jsStorage()
this.hasher = hasher || new mimcHasher()
this.n_levels = n_levels
this.zero_values = []
this.totalElements = 0
let current_zero_value = zero_value || 0
this.zero_values.push(current_zero_value)
for (let i = 0; i < n_levels; i++) {
current_zero_value = this.hasher.hash(i, current_zero_value, current_zero_value)
this.zero_values.push(
current_zero_value.toString(),
)
}
if (defaultElements) {
let level = 0
this.totalElements = defaultElements.length
defaultElements.forEach((element, i) => {
this.storage.put(MerkleTree.index_to_key(prefix, level, i), element)
})
level++
let numberOfElementsInLevel = Math.ceil(defaultElements.length / 2)
for (level; level <= this.n_levels; level++) {
for(let i = 0; i < numberOfElementsInLevel; i++) {
const leftKey = MerkleTree.index_to_key(prefix, level - 1, 2 * i)
const rightKey = MerkleTree.index_to_key(prefix, level - 1, 2 * i + 1)
const left = this.storage.get(leftKey)
const right = this.storage.get_or_element(rightKey, this.zero_values[level - 1])
const subRoot = this.hasher.hash(null, left, right)
this.storage.put(MerkleTree.index_to_key(prefix, level, i), subRoot)
}
numberOfElementsInLevel = Math.ceil(numberOfElementsInLevel / 2)
}
}
}
static index_to_key(prefix, level, index) {
const key = `${prefix}_tree_${level}_${index}`
return key
}
async root() {
let root = await this.storage.get_or_element(
MerkleTree.index_to_key(this.prefix, this.n_levels, 0),
this.zero_values[this.n_levels],
)
return root
}
async path(index) {
class PathTraverser {
constructor(prefix, storage, zero_values) {
this.prefix = prefix
this.storage = storage
this.zero_values = zero_values
this.path_elements = []
this.path_index = []
}
async handle_index(level, element_index, sibling_index) {
const sibling = await this.storage.get_or_element(
MerkleTree.index_to_key(this.prefix, level, sibling_index),
this.zero_values[level],
)
this.path_elements.push(sibling)
this.path_index.push(element_index % 2)
}
}
let traverser = new PathTraverser(this.prefix, this.storage, this.zero_values)
const root = await this.storage.get_or_element(
MerkleTree.index_to_key(this.prefix, this.n_levels, 0),
this.zero_values[this.n_levels],
)
const element = await this.storage.get_or_element(
MerkleTree.index_to_key(this.prefix, 0, index),
this.zero_values[0],
)
await this.traverse(index, traverser)
return {
root,
path_elements: traverser.path_elements,
path_index: traverser.path_index,
element
}
}
async update(index, element, insert = false) {
if (!insert && index >= this.totalElements) {
throw Error('Use insert method for new elements.')
} else if(insert && index < this.totalElements) {
throw Error('Use update method for existing elements.')
}
try {
class UpdateTraverser {
constructor(prefix, storage, hasher, element, zero_values) {
this.prefix = prefix
this.current_element = element
this.zero_values = zero_values
this.storage = storage
this.hasher = hasher
this.key_values_to_put = []
}
async handle_index(level, element_index, sibling_index) {
if (level == 0) {
this.original_element = await this.storage.get_or_element(
MerkleTree.index_to_key(this.prefix, level, element_index),
this.zero_values[level],
)
}
const sibling = await this.storage.get_or_element(
MerkleTree.index_to_key(this.prefix, level, sibling_index),
this.zero_values[level],
)
let left, right
if (element_index % 2 == 0) {
left = this.current_element
right = sibling
} else {
left = sibling
right = this.current_element
}
this.key_values_to_put.push({
key: MerkleTree.index_to_key(this.prefix, level, element_index),
value: this.current_element,
})
this.current_element = this.hasher.hash(level, left, right)
}
}
let traverser = new UpdateTraverser(
this.prefix,
this.storage,
this.hasher,
element,
this.zero_values
)
await this.traverse(index, traverser)
traverser.key_values_to_put.push({
key: MerkleTree.index_to_key(this.prefix, this.n_levels, 0),
value: traverser.current_element,
})
await this.storage.put_batch(traverser.key_values_to_put)
} catch(e) {
console.error(e)
}
}
async insert(element) {
const index = this.totalElements
await this.update(index, element, true)
this.totalElements++
}
async traverse(index, handler) {
let current_index = index
for (let i = 0; i < this.n_levels; i++) {
let sibling_index = current_index
if (current_index % 2 == 0) {
sibling_index += 1
} else {
sibling_index -= 1
}
await handler.handle_index(i, current_index, sibling_index)
current_index = Math.floor(current_index / 2)
}
}
getIndexByElement(element) {
for(let i = this.totalElements - 1; i >= 0; i--) {
const elementFromTree = this.storage.get(MerkleTree.index_to_key(this.prefix, 0, i))
if (elementFromTree === element) {
return i
}
}
return false
}
}
module.exports = MerkleTree

View File

@ -1,13 +0,0 @@
const circomlib = require('circomlib')
const mimcsponge = circomlib.mimcsponge
const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt
class MimcSpongeHasher {
hash(level, left, right) {
return mimcsponge.multiHash([bigInt(left), bigInt(right)]).toString()
}
}
module.exports = MimcSpongeHasher

View File

@ -1,39 +0,0 @@
class JsStorage {
constructor() {
this.db = {}
}
get(key) {
return this.db[key]
}
get_or_element(key, defaultElement) {
const element = this.db[key]
if (element === undefined) {
return defaultElement
} else {
return element
}
}
put(key, value) {
if (key === undefined || value === undefined) {
throw Error('key or value is undefined')
}
this.db[key] = value
}
del(key) {
delete this.db[key]
}
put_batch(key_values) {
key_values.forEach(element => {
this.db[element.key] = element.value
})
}
}
module.exports = JsStorage

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,21 @@
/* global artifacts */
require('dotenv').config({ path: '../.env' })
const ETHTornado = artifacts.require('ETHTornado')
const Verifier = artifacts.require('Verifier')
const Hasher = artifacts.require('Hasher')
module.exports = function (deployer) {
return deployer.then(async () => {
const { MERKLE_TREE_HEIGHT, ETH_AMOUNT } = process.env
const verifier = await Verifier.deployed()
const hasher = await Hasher.deployed()
const tornado = await deployer.deploy(
ETHTornado,
verifier.address,
hasher.address,
ETH_AMOUNT,
MERKLE_TREE_HEIGHT,
)
console.log('ETHTornado address', tornado.address)
})
}

View File

@ -1,17 +0,0 @@
/* global artifacts */
require('dotenv').config({ path: '../.env' })
const Mixer = artifacts.require('Mixer')
const Verifier = artifacts.require('Verifier')
const MiMC = artifacts.require('MiMC')
module.exports = function(deployer, network, accounts) {
return deployer.then(async () => {
const { MERKLE_TREE_HEIGHT, AMOUNT, EMPTY_ELEMENT } = process.env
const verifier = await Verifier.deployed()
const miMC = await MiMC.deployed()
await Mixer.link(MiMC, miMC.address)
const mixer = await deployer.deploy(Mixer, verifier.address, AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, accounts[0])
console.log('Mixer\'s address ', mixer.address)
})
}

View File

@ -0,0 +1,28 @@
/* global artifacts */
require('dotenv').config({ path: '../.env' })
const ERC20Tornado = artifacts.require('ERC20Tornado')
const Verifier = artifacts.require('Verifier')
const Hasher = artifacts.require('Hasher')
const ERC20Mock = artifacts.require('ERC20Mock')
module.exports = function (deployer) {
return deployer.then(async () => {
const { MERKLE_TREE_HEIGHT, ERC20_TOKEN, TOKEN_AMOUNT } = process.env
const verifier = await Verifier.deployed()
const hasher = await Hasher.deployed()
let token = ERC20_TOKEN
if (token === '') {
const tokenInstance = await deployer.deploy(ERC20Mock)
token = tokenInstance.address
}
const tornado = await deployer.deploy(
ERC20Tornado,
verifier.address,
hasher.address,
TOKEN_AMOUNT,
MERKLE_TREE_HEIGHT,
token,
)
console.log('ERC20Tornado address', tornado.address)
})
}

11070
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,37 +10,55 @@
"build:circuit:contract": "npx snarkjs generateverifier -v build/circuits/Verifier.sol --vk build/circuits/withdraw_verification_key.json",
"build:circuit": "mkdir -p build/circuits && npm run build:circuit:compile && npm run build:circuit:setup && npm run build:circuit:bin && npm run build:circuit:contract",
"build:contract": "npx truffle compile",
"build:browserify": "npx browserify src/cli.js -o index.js --exclude worker_threads",
"build": "npm run build:circuit && npm run build:contract && npm run build:browserify",
"browserify": "npm run build:browserify",
"test": "npx truffle test",
"migrate": "npx truffle migrate --network kovan --reset",
"migrate:mainnet": "npx truffle migrate --network mainnet",
"migrate": "npm run migrate:kovan",
"migrate:dev": "npx truffle migrate --network development --reset",
"browserify": "npx browserify cli.js -o index.js --exclude worker_threads",
"eslint": "npx eslint --ignore-path .gitignore .",
"flat": "truffle-flattener contracts/Mixer.sol > Mixer_flat.sol"
"migrate:kovan": "npx truffle migrate --network kovan --reset",
"migrate:rinkeby": "npx truffle migrate --network rinkeby --reset",
"migrate:mainnet": "npx truffle migrate --network mainnet",
"eslint": "eslint --ext .js --ignore-path .gitignore .",
"prettier:check": "prettier --check . --config .prettierrc",
"prettier:fix": "prettier --write . --config .prettierrc",
"lint": "yarn eslint && yarn prettier:check",
"flat": "npx truffle-flattener contracts/ETHTornado.sol > ETHTornado_flat.sol && npx truffle-flattener contracts/ERC20Tornado.sol > ERC20Tornado_flat.sol",
"download": "node scripts/downloadKeys.js",
"coverage": "yarn truffle run coverage"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@openzeppelin/contracts": "^3.4.1",
"@truffle/contract": "^4.0.39",
"@truffle/hdwallet-provider": "^1.0.24",
"axios": "^0.19.0",
"babel-eslint": "^10.1.0",
"bn-chai": "^1.0.1",
"browserify": "^16.3.0",
"browserify": "^16.5.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"circom": "0.0.30",
"circomlib": "^0.0.10",
"dotenv": "^8.0.0",
"eslint": "^6.0.1",
"ganache-cli": "^6.4.5",
"snarkjs": "^0.1.16",
"truffle": "^5.0.27",
"truffle-artifactor": "^4.0.23",
"truffle-contract": "^4.0.24",
"truffle-hdwallet-provider": "^1.0.14",
"web3": "^1.0.0-beta.55",
"web3-utils": "^1.0.0-beta.55",
"websnark": "git+https://github.com/peppersec/websnark.git#ed6a4d8a6fb081a62af26820980046bbb602d559"
},
"devDependencies": {
"truffle-flattener": "^1.4.0"
"circom": "^0.0.35",
"circomlib": "git+https://github.com/tornadocash/circomlib.git#c372f14d324d57339c88451834bf2824e73bbdbc",
"commander": "^4.1.1",
"dotenv": "^8.2.0",
"eslint": "^7.19.0",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-prettier": "^3.3.1",
"eth-json-rpc-filters": "^4.1.1",
"fixed-merkle-tree": "^0.6.0",
"ganache-cli": "^6.7.0",
"prettier": "^2.2.1",
"prettier-plugin-solidity": "^1.0.0-beta.3",
"snarkjs": "git+https://github.com/tornadocash/snarkjs.git#869181cfaf7526fe8972073d31655493a04326d5",
"solhint-plugin-prettier": "^0.0.5",
"truffle": "^5.1.67",
"truffle-flattener": "^1.4.2",
"web3": "^1.3.4",
"web3-utils": "^1.3.4",
"websnark": "git+https://github.com/tornadocash/websnark.git#4c0af6a8b65aabea3c09f377f63c44e7a58afa6d",
"solidity-coverage": "^0.7.20"
}
}

21
scripts/compileHasher.js Normal file
View File

@ -0,0 +1,21 @@
// Generates Hasher artifact at compile-time using Truffle's external compiler
// mechanism
const path = require('path')
const fs = require('fs')
const genContract = require('circomlib/src/mimcsponge_gencontract.js')
// where Truffle will expect to find the results of the external compiler
// command
const outputPath = path.join(__dirname, '..', 'build', 'Hasher.json')
function main() {
const contract = {
contractName: 'Hasher',
abi: genContract.abi,
bytecode: genContract.createCode('mimcsponge', 220),
}
fs.writeFileSync(outputPath, JSON.stringify(contract))
}
main()

43
scripts/downloadKeys.js Normal file
View File

@ -0,0 +1,43 @@
const axios = require('axios')
const path = require('path')
const fs = require('fs')
const files = ['withdraw.json', 'withdraw_proving_key.bin', 'Verifier.sol', 'withdraw_verification_key.json']
const circuitsPath = __dirname + '/../build/circuits'
const contractsPath = __dirname + '/../build/contracts'
async function downloadFile({ url, path }) {
const writer = fs.createWriteStream(path)
const response = await axios({
url,
method: 'GET',
responseType: 'stream',
})
response.data.pipe(writer)
return new Promise((resolve, reject) => {
writer.on('finish', resolve)
writer.on('error', reject)
})
}
async function main() {
const release = await axios.get('https://api.github.com/repos/tornadocash/tornado-core/releases/latest')
const { assets } = release.data
if (!fs.existsSync(circuitsPath)) {
fs.mkdirSync(circuitsPath, { recursive: true })
fs.mkdirSync(contractsPath, { recursive: true })
}
for (let asset of assets) {
if (files.includes(asset.name)) {
console.log(`Downloading ${asset.name} ...`)
await downloadFile({
url: asset.browser_download_url,
path: path.resolve(__dirname, circuitsPath, asset.name),
})
}
}
}
main()

View File

@ -6,7 +6,7 @@ function send(method, params = []) {
jsonrpc: '2.0',
id: Date.now(),
method,
params
params,
}, (err, res) => {
return err ? reject(err) : resolve(res)
})
@ -17,6 +17,10 @@ const takeSnapshot = async () => {
return await send('evm_snapshot')
}
const traceTransaction = async (tx) => {
return await send('debug_traceTransaction', [tx, {}])
}
const revertSnapshot = async (id) => {
await send('evm_revert', [id])
}
@ -44,4 +48,5 @@ module.exports = {
minerStop,
minerStart,
increaseTime,
traceTransaction,
}

640
src/cli.js Executable file
View File

@ -0,0 +1,640 @@
#!/usr/bin/env node
// Temporary demo client
// Works both in browser and node.js
require('dotenv').config()
const fs = require('fs')
const axios = require('axios')
const assert = require('assert')
const snarkjs = require('snarkjs')
const crypto = require('crypto')
const circomlib = require('circomlib')
const bigInt = snarkjs.bigInt
const merkleTree = require('fixed-merkle-tree')
const Web3 = require('web3')
const buildGroth16 = require('websnark/src/groth16')
const websnarkUtils = require('websnark/src/utils')
const { toWei, fromWei, toBN, BN } = require('web3-utils')
const config = require('./config')
const program = require('commander')
let web3, tornado, circuit, proving_key, groth16, erc20, senderAccount, netId
let MERKLE_TREE_HEIGHT, ETH_AMOUNT, TOKEN_AMOUNT, PRIVATE_KEY
/** Whether we are in a browser or node.js */
const inBrowser = (typeof window !== 'undefined')
let isLocalRPC = false
/** Generate random number of specified byte length */
const rbigint = nbytes => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
/** Compute pedersen hash */
const pedersenHash = data => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
/** BigNumber to hex string of specified length */
function toHex(number, length = 32) {
const str = number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)
return '0x' + str.padStart(length * 2, '0')
}
/** Display ETH account balance */
async function printETHBalance({ address, name }) {
console.log(`${name} ETH balance is`, web3.utils.fromWei(await web3.eth.getBalance(address)))
}
/** Display ERC20 account balance */
async function printERC20Balance({ address, name, tokenAddress }) {
const erc20ContractJson = require(__dirname + '/../build/contracts/ERC20Mock.json')
erc20 = tokenAddress ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : erc20
console.log(`${name} Token Balance is`, web3.utils.fromWei(await erc20.methods.balanceOf(address).call()))
}
/**
* Create deposit object from secret and nullifier
*/
function createDeposit({ nullifier, secret }) {
const deposit = { nullifier, secret }
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
deposit.commitment = pedersenHash(deposit.preimage)
deposit.commitmentHex = toHex(deposit.commitment)
deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
deposit.nullifierHex = toHex(deposit.nullifierHash)
return deposit
}
/**
* Make a deposit
* @param currency Сurrency
* @param amount Deposit amount
*/
async function deposit({ currency, amount }) {
const deposit = createDeposit({ nullifier: rbigint(31), secret: rbigint(31) })
const note = toHex(deposit.preimage, 62)
const noteString = `tornado-${currency}-${amount}-${netId}-${note}`
console.log(`Your note: ${noteString}`)
if (currency === 'eth') {
await printETHBalance({ address: tornado._address, name: 'Tornado' })
await printETHBalance({ address: senderAccount, name: 'Sender account' })
const value = isLocalRPC ? ETH_AMOUNT : fromDecimals({ amount, decimals: 18 })
console.log('Submitting deposit transaction')
await tornado.methods.deposit(toHex(deposit.commitment)).send({ value, from: senderAccount, gas: 2e6 })
await printETHBalance({ address: tornado._address, name: 'Tornado' })
await printETHBalance({ address: senderAccount, name: 'Sender account' })
} else { // a token
await printERC20Balance({ address: tornado._address, name: 'Tornado' })
await printERC20Balance({ address: senderAccount, name: 'Sender account' })
const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
const tokenAmount = isLocalRPC ? TOKEN_AMOUNT : fromDecimals({ amount, decimals })
if (isLocalRPC) {
console.log('Minting some test tokens to deposit')
await erc20.methods.mint(senderAccount, tokenAmount).send({ from: senderAccount, gas: 2e6 })
}
const allowance = await erc20.methods.allowance(senderAccount, tornado._address).call({ from: senderAccount })
console.log('Current allowance is', fromWei(allowance))
if (toBN(allowance).lt(toBN(tokenAmount))) {
console.log('Approving tokens for deposit')
await erc20.methods.approve(tornado._address, tokenAmount).send({ from: senderAccount, gas: 1e6 })
}
console.log('Submitting deposit transaction')
await tornado.methods.deposit(toHex(deposit.commitment)).send({ from: senderAccount, gas: 2e6 })
await printERC20Balance({ address: tornado._address, name: 'Tornado' })
await printERC20Balance({ address: senderAccount, name: 'Sender account' })
}
return noteString
}
/**
* Generate merkle tree for a deposit.
* Download deposit events from the tornado, reconstructs merkle tree, finds our deposit leaf
* in it and generates merkle proof
* @param deposit Deposit object
*/
async function generateMerkleProof(deposit) {
// Get all deposit events from smart contract and assemble merkle tree from them
console.log('Getting current state from tornado contract')
const events = await tornado.getPastEvents('Deposit', { fromBlock: 0, toBlock: 'latest' })
const leaves = events
.sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order
.map(e => e.returnValues.commitment)
const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves)
// Find current commitment in the tree
const depositEvent = events.find(e => e.returnValues.commitment === toHex(deposit.commitment))
const leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
// Validate that our data is correct
const root = tree.root()
const isValidRoot = await tornado.methods.isKnownRoot(toHex(root)).call()
const isSpent = await tornado.methods.isSpent(toHex(deposit.nullifierHash)).call()
assert(isValidRoot === true, 'Merkle tree is corrupted')
assert(isSpent === false, 'The note is already spent')
assert(leafIndex >= 0, 'The deposit is not found in the tree')
// Compute merkle proof of our commitment
const { pathElements, pathIndices } = tree.path(leafIndex)
return { pathElements, pathIndices, root: tree.root() }
}
/**
* Generate SNARK proof for withdrawal
* @param deposit Deposit object
* @param recipient Funds recipient
* @param relayer Relayer address
* @param fee Relayer fee
* @param refund Receive ether for exchanged tokens
*/
async function generateProof({ deposit, recipient, relayerAddress = 0, fee = 0, refund = 0 }) {
// Compute merkle proof of our commitment
const { root, pathElements, pathIndices } = await generateMerkleProof(deposit)
// Prepare circuit input
const input = {
// Public snark inputs
root: root,
nullifierHash: deposit.nullifierHash,
recipient: bigInt(recipient),
relayer: bigInt(relayerAddress),
fee: bigInt(fee),
refund: bigInt(refund),
// Private snark inputs
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
}
console.log('Generating SNARK proof')
console.time('Proof time')
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
console.timeEnd('Proof time')
const args = [
toHex(input.root),
toHex(input.nullifierHash),
toHex(input.recipient, 20),
toHex(input.relayer, 20),
toHex(input.fee),
toHex(input.refund),
]
return { proof, args }
}
/**
* Do an ETH withdrawal
* @param noteString Note to withdraw
* @param recipient Recipient address
*/
async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund = '0' }) {
if (currency === 'eth' && refund !== '0') {
throw new Error('The ETH purchase is supposted to be 0 for ETH withdrawals')
}
refund = toWei(refund)
if (relayerURL) {
if (relayerURL.endsWith('.eth')) {
throw new Error('ENS name resolving is not supported. Please provide DNS name of the relayer. See instuctions in README.md')
}
const relayerStatus = await axios.get(relayerURL + '/status')
const { relayerAddress, netId, gasPrices, ethPrices, relayerServiceFee } = relayerStatus.data
assert(netId === await web3.eth.net.getId() || netId === '*', 'This relay is for different network')
console.log('Relay address: ', relayerAddress)
const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
const fee = calculateFee({ gasPrices, currency, amount, refund, ethPrices, relayerServiceFee, decimals })
if (fee.gt(fromDecimals({ amount, decimals }))) {
throw new Error('Too high refund')
}
const { proof, args } = await generateProof({ deposit, recipient, relayerAddress, fee, refund })
console.log('Sending withdraw transaction through relay')
try {
const relay = await axios.post(relayerURL + '/relay', { contract: tornado._address, proof, args })
if (netId === 1 || netId === 42) {
console.log(`Transaction submitted through the relay. View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${relay.data.txHash}`)
} else {
console.log(`Transaction submitted through the relay. The transaction hash is ${relay.data.txHash}`)
}
const receipt = await waitForTxReceipt({ txHash: relay.data.txHash })
console.log('Transaction mined in block', receipt.blockNumber)
} catch (e) {
if (e.response) {
console.error(e.response.data.error)
} else {
console.error(e.message)
}
}
} else { // using private key
const { proof, args } = await generateProof({ deposit, recipient, refund })
console.log('Submitting withdraw transaction')
await tornado.methods.withdraw(proof, ...args).send({ from: senderAccount, value: refund.toString(), gas: 1e6 })
.on('transactionHash', function (txHash) {
if (netId === 1 || netId === 42) {
console.log(`View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${txHash}`)
} else {
console.log(`The transaction hash is ${txHash}`)
}
}).on('error', function (e) {
console.error('on transactionHash error', e.message)
})
}
console.log('Done')
}
function fromDecimals({ amount, decimals }) {
amount = amount.toString()
let ether = amount.toString()
const base = new BN('10').pow(new BN(decimals))
const baseLength = base.toString(10).length - 1 || 1
const negative = ether.substring(0, 1) === '-'
if (negative) {
ether = ether.substring(1)
}
if (ether === '.') {
throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, invalid value')
}
// Split it into a whole and fractional part
const comps = ether.split('.')
if (comps.length > 2) {
throw new Error(
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal points',
)
}
let whole = comps[0]
let fraction = comps[1]
if (!whole) {
whole = '0'
}
if (!fraction) {
fraction = '0'
}
if (fraction.length > baseLength) {
throw new Error(
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal places',
)
}
while (fraction.length < baseLength) {
fraction += '0'
}
whole = new BN(whole)
fraction = new BN(fraction)
let wei = whole.mul(base).add(fraction)
if (negative) {
wei = wei.mul(negative)
}
return new BN(wei.toString(10), 10)
}
function toDecimals(value, decimals, fixed) {
const zero = new BN(0)
const negative1 = new BN(-1)
decimals = decimals || 18
fixed = fixed || 7
value = new BN(value)
const negative = value.lt(zero)
const base = new BN('10').pow(new BN(decimals))
const baseLength = base.toString(10).length - 1 || 1
if (negative) {
value = value.mul(negative1)
}
let fraction = value.mod(base).toString(10)
while (fraction.length < baseLength) {
fraction = `0${fraction}`
}
fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1]
const whole = value.div(base).toString(10)
value = `${whole}${fraction === '0' ? '' : `.${fraction}`}`
if (negative) {
value = `-${value}`
}
if (fixed) {
value = value.slice(0, fixed)
}
return value
}
function getCurrentNetworkName() {
switch (netId) {
case 1:
return ''
case 42:
return 'kovan.'
}
}
function calculateFee({ gasPrices, currency, amount, refund, ethPrices, relayerServiceFee, decimals }) {
const decimalsPoint = Math.floor(relayerServiceFee) === Number(relayerServiceFee) ?
0 :
relayerServiceFee.toString().split('.')[1].length
const roundDecimal = 10 ** decimalsPoint
const total = toBN(fromDecimals({ amount, decimals }))
const feePercent = total.mul(toBN(relayerServiceFee * roundDecimal)).div(toBN(roundDecimal * 100))
const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(5e5))
let desiredFee
switch (currency) {
case 'eth': {
desiredFee = expense.add(feePercent)
break
}
default: {
desiredFee = expense.add(toBN(refund))
.mul(toBN(10 ** decimals))
.div(toBN(ethPrices[currency]))
desiredFee = desiredFee.add(feePercent)
break
}
}
return desiredFee
}
/**
* Waits for transaction to be mined
* @param txHash Hash of transaction
* @param attempts
* @param delay
*/
function waitForTxReceipt({ txHash, attempts = 60, delay = 1000 }) {
return new Promise((resolve, reject) => {
const checkForTx = async (txHash, retryAttempt = 0) => {
const result = await web3.eth.getTransactionReceipt(txHash)
if (!result || !result.blockNumber) {
if (retryAttempt <= attempts) {
setTimeout(() => checkForTx(txHash, retryAttempt + 1), delay)
} else {
reject(new Error('tx was not mined'))
}
} else {
resolve(result)
}
}
checkForTx(txHash)
})
}
/**
* Parses Tornado.cash note
* @param noteString the note
*/
function parseNote(noteString) {
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g
const match = noteRegex.exec(noteString)
if (!match) {
throw new Error('The note has invalid format')
}
const buf = Buffer.from(match.groups.note, 'hex')
const nullifier = bigInt.leBuff2int(buf.slice(0, 31))
const secret = bigInt.leBuff2int(buf.slice(31, 62))
const deposit = createDeposit({ nullifier, secret })
const netId = Number(match.groups.netId)
return { currency: match.groups.currency, amount: match.groups.amount, netId, deposit }
}
async function loadDepositData({ deposit }) {
try {
const eventWhenHappened = await tornado.getPastEvents('Deposit', {
filter: {
commitment: deposit.commitmentHex,
},
fromBlock: 0,
toBlock: 'latest',
})
if (eventWhenHappened.length === 0) {
throw new Error('There is no related deposit, the note is invalid')
}
const { timestamp } = eventWhenHappened[0].returnValues
const txHash = eventWhenHappened[0].transactionHash
const isSpent = await tornado.methods.isSpent(deposit.nullifierHex).call()
const receipt = await web3.eth.getTransactionReceipt(txHash)
return { timestamp, txHash, isSpent, from: receipt.from, commitment: deposit.commitmentHex }
} catch (e) {
console.error('loadDepositData', e)
}
return {}
}
async function loadWithdrawalData({ amount, currency, deposit }) {
try {
const events = await tornado.getPastEvents('Withdrawal', {
fromBlock: 0,
toBlock: 'latest',
})
const withdrawEvent = events.filter((event) => {
return event.returnValues.nullifierHash === deposit.nullifierHex
})[0]
const fee = withdrawEvent.returnValues.fee
const decimals = config.deployments[`netId${netId}`][currency].decimals
const withdrawalAmount = toBN(fromDecimals({ amount, decimals })).sub(
toBN(fee),
)
const { timestamp } = await web3.eth.getBlock(withdrawEvent.blockHash)
return {
amount: toDecimals(withdrawalAmount, decimals, 9),
txHash: withdrawEvent.transactionHash,
to: withdrawEvent.returnValues.to,
timestamp,
nullifier: deposit.nullifierHex,
fee: toDecimals(fee, decimals, 9),
}
} catch (e) {
console.error('loadWithdrawalData', e)
}
}
/**
* Init web3, contracts, and snark
*/
async function init({ rpc, noteNetId, currency = 'dai', amount = '100' }) {
let contractJson, erc20ContractJson, erc20tornadoJson, tornadoAddress, tokenAddress
// TODO do we need this? should it work in browser really?
if (inBrowser) {
// Initialize using injected web3 (Metamask)
// To assemble web version run `npm run browserify`
web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 })
contractJson = await (await fetch('build/contracts/ETHTornado.json')).json()
circuit = await (await fetch('build/circuits/withdraw.json')).json()
proving_key = await (await fetch('build/circuits/withdraw_proving_key.bin')).arrayBuffer()
MERKLE_TREE_HEIGHT = 20
ETH_AMOUNT = 1e18
TOKEN_AMOUNT = 1e19
senderAccount = (await web3.eth.getAccounts())[0]
} else {
// Initialize from local node
web3 = new Web3(rpc, null, { transactionConfirmationBlocks: 1 })
contractJson = require(__dirname + '/../build/contracts/ETHTornado.json')
circuit = require(__dirname + '/../build/circuits/withdraw.json')
proving_key = fs.readFileSync(__dirname + '/../build/circuits/withdraw_proving_key.bin').buffer
MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT || 20
ETH_AMOUNT = process.env.ETH_AMOUNT
TOKEN_AMOUNT = process.env.TOKEN_AMOUNT
PRIVATE_KEY = process.env.PRIVATE_KEY
if (PRIVATE_KEY) {
const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY)
web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY)
web3.eth.defaultAccount = account.address
senderAccount = account.address
} else {
console.log('Warning! PRIVATE_KEY not found. Please provide PRIVATE_KEY in .env file if you deposit')
}
erc20ContractJson = require(__dirname + '/../build/contracts/ERC20Mock.json')
erc20tornadoJson = require(__dirname + '/../build/contracts/ERC20Tornado.json')
}
// groth16 initialises a lot of Promises that will never be resolved, that's why we need to use process.exit to terminate the CLI
groth16 = await buildGroth16()
netId = await web3.eth.net.getId()
if (noteNetId && Number(noteNetId) !== netId) {
throw new Error('This note is for a different network. Specify the --rpc option explicitly')
}
isLocalRPC = netId > 42
if (isLocalRPC) {
tornadoAddress = currency === 'eth' ? contractJson.networks[netId].address : erc20tornadoJson.networks[netId].address
tokenAddress = currency !== 'eth' ? erc20ContractJson.networks[netId].address : null
senderAccount = (await web3.eth.getAccounts())[0]
} else {
try {
tornadoAddress = config.deployments[`netId${netId}`][currency].instanceAddress[amount]
if (!tornadoAddress) {
throw new Error()
}
tokenAddress = config.deployments[`netId${netId}`][currency].tokenAddress
} catch (e) {
console.error('There is no such tornado instance, check the currency and amount you provide')
process.exit(1)
}
}
tornado = new web3.eth.Contract(contractJson.abi, tornadoAddress)
erc20 = currency !== 'eth' ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : {}
}
async function main() {
if (inBrowser) {
const instance = { currency: 'eth', amount: '0.1' }
await init(instance)
window.deposit = async () => {
await deposit(instance)
}
window.withdraw = async () => {
const noteString = prompt('Enter the note to withdraw')
const recipient = (await web3.eth.getAccounts())[0]
const { currency, amount, netId, deposit } = parseNote(noteString)
await init({ noteNetId: netId, currency, amount })
await withdraw({ deposit, currency, amount, recipient })
}
} else {
program
.option('-r, --rpc <URL>', 'The RPC, CLI should interact with', 'http://localhost:8545')
.option('-R, --relayer <URL>', 'Withdraw via relayer')
program
.command('deposit <currency> <amount>')
.description('Submit a deposit of specified currency and amount from default eth account and return the resulting note. The currency is one of (ETH|DAI|cDAI|USDC|cUSDC|USDT). The amount depends on currency, see config.js file or visit https://tornado.cash.')
.action(async (currency, amount) => {
currency = currency.toLowerCase()
await init({ rpc: program.rpc, currency, amount })
await deposit({ currency, amount })
})
program
.command('withdraw <note> <recipient> [ETH_purchase]')
.description('Withdraw a note to a recipient account using relayer or specified private key. You can exchange some of your deposit`s tokens to ETH during the withdrawal by specifing ETH_purchase (e.g. 0.01) to pay for gas in future transactions. Also see the --relayer option.')
.action(async (noteString, recipient, refund) => {
const { currency, amount, netId, deposit } = parseNote(noteString)
await init({ rpc: program.rpc, noteNetId: netId, currency, amount })
await withdraw({ deposit, currency, amount, recipient, refund, relayerURL: program.relayer })
})
program
.command('balance <address> [token_address]')
.description('Check ETH and ERC20 balance')
.action(async (address, tokenAddress) => {
await init({ rpc: program.rpc })
await printETHBalance({ address, name: '' })
if (tokenAddress) {
await printERC20Balance({ address, name: '', tokenAddress })
}
})
program
.command('compliance <note>')
.description('Shows the deposit and withdrawal of the provided note. This might be necessary to show the origin of assets held in your withdrawal address.')
.action(async (noteString) => {
const { currency, amount, netId, deposit } = parseNote(noteString)
await init({ rpc: program.rpc, noteNetId: netId, currency, amount })
const depositInfo = await loadDepositData({ deposit })
const depositDate = new Date(depositInfo.timestamp * 1000)
console.log('\n=============Deposit=================')
console.log('Deposit :', amount, currency)
console.log('Date :', depositDate.toLocaleDateString(), depositDate.toLocaleTimeString())
console.log('From :', `https://${getCurrentNetworkName()}etherscan.io/address/${depositInfo.from}`)
console.log('Transaction :', `https://${getCurrentNetworkName()}etherscan.io/tx/${depositInfo.txHash}`)
console.log('Commitment :', depositInfo.commitment)
if (deposit.isSpent) {
console.log('The note was not spent')
}
const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit })
const withdrawalDate = new Date(withdrawInfo.timestamp * 1000)
console.log('\n=============Withdrawal==============')
console.log('Withdrawal :', withdrawInfo.amount, currency)
console.log('Relayer Fee :', withdrawInfo.fee, currency)
console.log('Date :', withdrawalDate.toLocaleDateString(), withdrawalDate.toLocaleTimeString())
console.log('To :', `https://${getCurrentNetworkName()}etherscan.io/address/${withdrawInfo.to}`)
console.log('Transaction :', `https://${getCurrentNetworkName()}etherscan.io/tx/${withdrawInfo.txHash}`)
console.log('Nullifier :', withdrawInfo.nullifier)
})
program
.command('test')
.description('Perform an automated test. It deposits and withdraws one ETH and one ERC20 note. Uses ganache.')
.action(async () => {
console.log('Start performing ETH deposit-withdraw test')
let currency = 'eth'
let amount = '0.1'
await init({ rpc: program.rpc, currency, amount })
let noteString = await deposit({ currency, amount })
let parsedNote = parseNote(noteString)
await withdraw({ deposit: parsedNote.deposit, currency, amount, recipient: senderAccount, relayerURL: program.relayer })
console.log('\nStart performing DAI deposit-withdraw test')
currency = 'dai'
amount = '100'
await init({ rpc: program.rpc, currency, amount })
noteString = await deposit({ currency, amount })
; (parsedNote = parseNote(noteString))
await withdraw({ deposit: parsedNote.deposit, currency, amount, recipient: senderAccount, refund: '0.02', relayerURL: program.relayer })
})
try {
await program.parseAsync(process.argv)
process.exit(0)
} catch (e) {
console.log('Error:', e)
process.exit(1)
}
}
}
main()

140
src/config.js Normal file
View File

@ -0,0 +1,140 @@
require('dotenv').config()
module.exports = {
deployments: {
netId1: {
eth: {
instanceAddress: {
0.1: '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
1: '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
10: '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
100: '0xA160cdAB225685dA1d56aa342Ad8841c3b53f291',
},
symbol: 'ETH',
decimals: 18,
},
dai: {
instanceAddress: {
100: '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
1000: '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
10000: '0xF60dD140cFf0706bAE9Cd734Ac3ae76AD9eBC32A',
100000: undefined,
},
tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
symbol: 'DAI',
decimals: 18,
},
cdai: {
instanceAddress: {
5000: '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
50000: '0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659',
500000: '0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00',
5000000: undefined,
},
tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643',
symbol: 'cDAI',
decimals: 8,
},
usdc: {
instanceAddress: {
100: '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
1000: '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
10000: '0xD691F27f38B395864Ea86CfC7253969B409c362d',
100000: undefined,
},
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
symbol: 'USDC',
decimals: 6,
},
cusdc: {
instanceAddress: {
5000: '0xaEaaC358560e11f52454D997AAFF2c5731B6f8a6',
50000: '0x1356c899D8C9467C7f71C195612F8A395aBf2f0a',
500000: '0xA60C772958a3eD56c1F15dD055bA37AC8e523a0D',
5000000: undefined,
},
tokenAddress: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
symbol: 'cUSDC',
decimals: 8,
},
usdt: {
instanceAddress: {
100: '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
1000: '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
10000: '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a',
100000: '0x9AD122c22B14202B4490eDAf288FDb3C7cb3ff5E',
},
tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
symbol: 'USDT',
decimals: 6,
},
},
netId42: {
eth: {
instanceAddress: {
0.1: '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
1: '0xD6a6AC46d02253c938B96D12BE439F570227aE8E',
10: '0xe1BE96331391E519471100c3c1528B66B8F4e5a7',
100: '0xd037E0Ac98Dab2fCb7E296c69C6e52767Ae5414D',
},
symbol: 'ETH',
decimals: 18,
},
dai: {
instanceAddress: {
100: '0xdf2d3cC5F361CF95b3f62c4bB66deFe3FDE47e3D',
1000: '0xD96291dFa35d180a71964D0894a1Ae54247C4ccD',
10000: '0xb192794f72EA45e33C3DF6fe212B9c18f6F45AE3',
100000: undefined,
},
tokenAddress: '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa',
symbol: 'DAI',
decimals: 18,
},
cdai: {
instanceAddress: {
5000: '0x6Fc9386ABAf83147b3a89C36D422c625F44121C8',
50000: '0x7182EA067e0f050997444FCb065985Fd677C16b6',
500000: '0xC22ceFd90fbd1FdEeE554AE6Cc671179BC3b10Ae',
5000000: undefined,
},
tokenAddress: '0xe7bc397DBd069fC7d0109C0636d06888bb50668c',
symbol: 'cDAI',
decimals: 8,
},
usdc: {
instanceAddress: {
100: '0x137E2B6d185018e7f09f6cf175a970e7fC73826C',
1000: '0xcC7f1633A5068E86E3830e692e3e3f8f520525Af',
10000: '0x28C8f149a0ab8A9bdB006B8F984fFFCCE52ef5EF',
100000: undefined,
},
tokenAddress: '0x75B0622Cec14130172EaE9Cf166B92E5C112FaFF',
symbol: 'USDC',
decimals: 6,
},
cusdc: {
instanceAddress: {
5000: '0xc0648F28ABA385c8a1421Bbf1B59e3c474F89cB0',
50000: '0x0C53853379c6b1A7B74E0A324AcbDD5Eabd4981D',
500000: '0xf84016A0E03917cBe700D318EB1b7a53e6e3dEe1',
5000000: undefined,
},
tokenAddress: '0xcfC9bB230F00bFFDB560fCe2428b4E05F3442E35',
symbol: 'cUSDC',
decimals: 8,
},
usdt: {
instanceAddress: {
100: '0x327853Da7916a6A0935563FB1919A48843036b42',
1000: '0x531AA4DF5858EA1d0031Dad16e3274609DE5AcC0',
10000: '0x0958275F0362cf6f07D21373aEE0cf37dFe415dD',
100000: '0x14aEd24B67EaF3FF28503eB92aeb217C47514364',
},
tokenAddress: '0x03c5F29e9296006876d8DF210BCFfD7EA5Db1Cf1',
symbol: 'USDT',
decimals: 6,
},
},
},
}

176
src/minimal-demo.js Normal file
View File

@ -0,0 +1,176 @@
const fs = require('fs')
const assert = require('assert')
const { bigInt } = require('snarkjs')
const crypto = require('crypto')
const circomlib = require('circomlib')
const merkleTree = require('fixed-merkle-tree')
const Web3 = require('web3')
const buildGroth16 = require('websnark/src/groth16')
const websnarkUtils = require('websnark/src/utils')
const { toWei } = require('web3-utils')
let web3, contract, netId, circuit, proving_key, groth16
const MERKLE_TREE_HEIGHT = 20
const RPC_URL = 'https://kovan.infura.io/v3/0279e3bdf3ee49d0b547c643c2ef78ef'
const PRIVATE_KEY = 'ad5b6eb7ee88173fa43dedcff8b1d9024d03f6307a1143ecf04bea8ed40f283f' // 0x94462e71A887756704f0fb1c0905264d487972fE
const CONTRACT_ADDRESS = '0xD6a6AC46d02253c938B96D12BE439F570227aE8E'
const AMOUNT = '1'
// CURRENCY = 'ETH'
/** Generate random number of specified byte length */
const rbigint = (nbytes) => bigInt.leBuff2int(crypto.randomBytes(nbytes))
/** Compute pedersen hash */
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
/** BigNumber to hex string of specified length */
const toHex = (number, length = 32) =>
'0x' +
(number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)).padStart(length * 2, '0')
/**
* Create deposit object from secret and nullifier
*/
function createDeposit(nullifier, secret) {
let deposit = { nullifier, secret }
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
deposit.commitment = pedersenHash(deposit.preimage)
deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
return deposit
}
/**
* Make an ETH deposit
*/
async function deposit() {
const deposit = createDeposit(rbigint(31), rbigint(31))
console.log('Sending deposit transaction...')
const tx = await contract.methods
.deposit(toHex(deposit.commitment))
.send({ value: toWei(AMOUNT), from: web3.eth.defaultAccount, gas: 2e6 })
console.log(`https://kovan.etherscan.io/tx/${tx.transactionHash}`)
return `tornado-eth-${AMOUNT}-${netId}-${toHex(deposit.preimage, 62)}`
}
/**
* Do an ETH withdrawal
* @param note Note to withdraw
* @param recipient Recipient address
*/
async function withdraw(note, recipient) {
const deposit = parseNote(note)
const { proof, args } = await generateSnarkProof(deposit, recipient)
console.log('Sending withdrawal transaction...')
const tx = await contract.methods.withdraw(proof, ...args).send({ from: web3.eth.defaultAccount, gas: 1e6 })
console.log(`https://kovan.etherscan.io/tx/${tx.transactionHash}`)
}
/**
* Parses Tornado.cash note
* @param noteString the note
*/
function parseNote(noteString) {
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g
const match = noteRegex.exec(noteString)
// we are ignoring `currency`, `amount`, and `netId` for this minimal example
const buf = Buffer.from(match.groups.note, 'hex')
const nullifier = bigInt.leBuff2int(buf.slice(0, 31))
const secret = bigInt.leBuff2int(buf.slice(31, 62))
return createDeposit(nullifier, secret)
}
/**
* Generate merkle tree for a deposit.
* Download deposit events from the contract, reconstructs merkle tree, finds our deposit leaf
* in it and generates merkle proof
* @param deposit Deposit object
*/
async function generateMerkleProof(deposit) {
console.log('Getting contract state...')
const events = await contract.getPastEvents('Deposit', { fromBlock: 0, toBlock: 'latest' })
const leaves = events
.sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order
.map((e) => e.returnValues.commitment)
const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves)
// Find current commitment in the tree
let depositEvent = events.find((e) => e.returnValues.commitment === toHex(deposit.commitment))
let leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
// Validate that our data is correct (optional)
const isValidRoot = await contract.methods.isKnownRoot(toHex(tree.root())).call()
const isSpent = await contract.methods.isSpent(toHex(deposit.nullifierHash)).call()
assert(isValidRoot === true, 'Merkle tree is corrupted')
assert(isSpent === false, 'The note is already spent')
assert(leafIndex >= 0, 'The deposit is not found in the tree')
// Compute merkle proof of our commitment
const { pathElements, pathIndices } = tree.path(leafIndex)
return { pathElements, pathIndices, root: tree.root() }
}
/**
* Generate SNARK proof for withdrawal
* @param deposit Deposit object
* @param recipient Funds recipient
*/
async function generateSnarkProof(deposit, recipient) {
// Compute merkle proof of our commitment
const { root, pathElements, pathIndices } = await generateMerkleProof(deposit)
// Prepare circuit input
const input = {
// Public snark inputs
root: root,
nullifierHash: deposit.nullifierHash,
recipient: bigInt(recipient),
relayer: 0,
fee: 0,
refund: 0,
// Private snark inputs
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
}
console.log('Generating SNARK proof...')
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const args = [
toHex(input.root),
toHex(input.nullifierHash),
toHex(input.recipient, 20),
toHex(input.relayer, 20),
toHex(input.fee),
toHex(input.refund),
]
return { proof, args }
}
async function main() {
web3 = new Web3(new Web3.providers.HttpProvider(RPC_URL, { timeout: 5 * 60 * 1000 }), null, {
transactionConfirmationBlocks: 1,
})
circuit = require(__dirname + '/../build/circuits/withdraw.json')
proving_key = fs.readFileSync(__dirname + '/../build/circuits/withdraw_proving_key.bin').buffer
groth16 = await buildGroth16()
netId = await web3.eth.net.getId()
contract = new web3.eth.Contract(require('../build/contracts/ETHTornado.json').abi, CONTRACT_ADDRESS)
const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY)
web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY)
// eslint-disable-next-line require-atomic-updates
web3.eth.defaultAccount = account.address
const note = await deposit()
console.log('Deposited note:', note)
await withdraw(note, web3.eth.defaultAccount)
console.log('Done')
process.exit()
}
main()

505
test/ERC20Tornado.test.js Normal file
View File

@ -0,0 +1,505 @@
/* global artifacts, web3, contract */
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
const fs = require('fs')
const { toBN } = require('web3-utils')
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
const Tornado = artifacts.require('./ERC20Tornado.sol')
const BadRecipient = artifacts.require('./BadRecipient.sol')
const Token = artifacts.require('./ERC20Mock.sol')
const USDTToken = artifacts.require('./IUSDT.sol')
const { ETH_AMOUNT, TOKEN_AMOUNT, MERKLE_TREE_HEIGHT, ERC20_TOKEN } = process.env
const websnarkUtils = require('websnark/src/utils')
const buildGroth16 = require('websnark/src/groth16')
const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts
const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt
const crypto = require('crypto')
const circomlib = require('circomlib')
const MerkleTree = require('fixed-merkle-tree')
const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
const toFixedHex = (number, length = 32) =>
'0x' +
bigInt(number)
.toString(16)
.padStart(length * 2, '0')
const getRandomRecipient = () => rbigint(20)
function generateDeposit() {
let deposit = {
secret: rbigint(31),
nullifier: rbigint(31),
}
const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
deposit.commitment = pedersenHash(preimage)
return deposit
}
contract('ERC20Tornado', (accounts) => {
let tornado
let token
let usdtToken
let badRecipient
const sender = accounts[0]
const operator = accounts[0]
const levels = MERKLE_TREE_HEIGHT || 16
let tokenDenomination = TOKEN_AMOUNT || '1000000000000000000' // 1 ether
let snapshotId
let tree
const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
const refund = ETH_AMOUNT || '1000000000000000000' // 1 ether
let recipient = getRandomRecipient()
const relayer = accounts[1]
let groth16
let circuit
let proving_key
before(async () => {
tree = new MerkleTree(levels)
tornado = await Tornado.deployed()
if (ERC20_TOKEN) {
token = await Token.at(ERC20_TOKEN)
usdtToken = await USDTToken.at(ERC20_TOKEN)
} else {
token = await Token.deployed()
await token.mint(sender, tokenDenomination)
}
badRecipient = await BadRecipient.new()
snapshotId = await takeSnapshot()
groth16 = await buildGroth16()
circuit = require('../build/circuits/withdraw.json')
proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
})
describe('#constructor', () => {
it('should initialize', async () => {
const tokenFromContract = await tornado.token()
tokenFromContract.should.be.equal(token.address)
})
})
describe('#deposit', () => {
it('should work', async () => {
const commitment = toFixedHex(43)
await token.approve(tornado.address, tokenDenomination)
let { logs } = await tornado.deposit(commitment, { from: sender })
logs[0].event.should.be.equal('Deposit')
logs[0].args.commitment.should.be.equal(commitment)
logs[0].args.leafIndex.should.be.eq.BN(0)
})
it('should not allow to send ether on deposit', async () => {
const commitment = toFixedHex(43)
await token.approve(tornado.address, tokenDenomination)
let error = await tornado.deposit(commitment, { from: sender, value: 1e6 }).should.be.rejected
error.reason.should.be.equal('ETH value is supposed to be 0 for ERC20 instance')
})
})
describe('#withdraw', () => {
it('should work', async () => {
const deposit = generateDeposit()
const user = accounts[4]
tree.insert(deposit.commitment)
await token.mint(user, tokenDenomination)
const balanceUserBefore = await token.balanceOf(user)
await token.approve(tornado.address, tokenDenomination, { from: user })
// Uncomment to measure gas usage
// let gas = await tornado.deposit.estimateGas(toBN(deposit.commitment.toString()), { from: user, gasPrice: '0' })
// console.log('deposit gas:', gas)
await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
const balanceUserAfter = await token.balanceOf(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
const { pathElements, pathIndices } = tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer,
recipient,
fee,
refund,
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const balanceTornadoBefore = await token.balanceOf(tornado.address)
const balanceRelayerBefore = await token.balanceOf(relayer)
const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20))
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
const ethBalanceRelayerBefore = await web3.eth.getBalance(relayer)
let isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(false)
// Uncomment to measure gas usage
// gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
// console.log('withdraw gas:', gas)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
const balanceTornadoAfter = await token.balanceOf(tornado.address)
const balanceRelayerAfter = await token.balanceOf(relayer)
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
const balanceReceiverAfter = await token.balanceOf(toFixedHex(recipient, 20))
const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
const ethBalanceRelayerAfter = await web3.eth.getBalance(relayer)
const feeBN = toBN(fee.toString())
balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(tokenDenomination)))
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore).add(feeBN))
balanceReceiverAfter.should.be.eq.BN(
toBN(balanceReceiverBefore).add(toBN(tokenDenomination).sub(feeBN)),
)
ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore))
ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore).add(toBN(refund)))
ethBalanceRelayerAfter.should.be.eq.BN(toBN(ethBalanceRelayerBefore).sub(toBN(refund)))
logs[0].event.should.be.equal('Withdrawal')
logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash))
logs[0].args.relayer.should.be.eq.BN(relayer)
logs[0].args.fee.should.be.eq.BN(feeBN)
isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(true)
})
it('should return refund to the relayer is case of fail', async () => {
const deposit = generateDeposit()
const user = accounts[4]
recipient = bigInt(badRecipient.address)
tree.insert(deposit.commitment)
await token.mint(user, tokenDenomination)
const balanceUserBefore = await token.balanceOf(user)
await token.approve(tornado.address, tokenDenomination, { from: user })
await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
const balanceUserAfter = await token.balanceOf(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
const { pathElements, pathIndices } = tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer,
recipient,
fee,
refund,
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const balanceTornadoBefore = await token.balanceOf(tornado.address)
const balanceRelayerBefore = await token.balanceOf(relayer)
const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20))
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
const ethBalanceRelayerBefore = await web3.eth.getBalance(relayer)
let isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(false)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
const balanceTornadoAfter = await token.balanceOf(tornado.address)
const balanceRelayerAfter = await token.balanceOf(relayer)
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
const balanceReceiverAfter = await token.balanceOf(toFixedHex(recipient, 20))
const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
const ethBalanceRelayerAfter = await web3.eth.getBalance(relayer)
const feeBN = toBN(fee.toString())
balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(tokenDenomination)))
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore).add(feeBN))
balanceReceiverAfter.should.be.eq.BN(
toBN(balanceReceiverBefore).add(toBN(tokenDenomination).sub(feeBN)),
)
ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore))
ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore))
ethBalanceRelayerAfter.should.be.eq.BN(toBN(ethBalanceRelayerBefore))
logs[0].event.should.be.equal('Withdrawal')
logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash))
logs[0].args.relayer.should.be.eq.BN(relayer)
logs[0].args.fee.should.be.eq.BN(feeBN)
isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(true)
})
it('should reject with wrong refund value', async () => {
const deposit = generateDeposit()
const user = accounts[4]
tree.insert(deposit.commitment)
await token.mint(user, tokenDenomination)
await token.approve(tornado.address, tokenDenomination, { from: user })
await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
const { pathElements, pathIndices } = tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer,
recipient,
fee,
refund,
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
let { reason } = await tornado.withdraw(proof, ...args, { value: 1, from: relayer, gasPrice: '0' })
.should.be.rejected
reason.should.be.equal('Incorrect refund amount received by the contract')
;({ reason } = await tornado.withdraw(proof, ...args, {
value: toBN(refund).mul(toBN(2)),
from: relayer,
gasPrice: '0',
}).should.be.rejected)
reason.should.be.equal('Incorrect refund amount received by the contract')
})
it.skip('should work with REAL USDT', async () => {
// dont forget to specify your token in .env
// USDT decimals is 6, so TOKEN_AMOUNT=1000000
// and sent `tokenDenomination` to accounts[0] (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1)
// run ganache as
// ganache-cli --fork https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448@13147586 -d --keepAliveTimeout 20
const deposit = generateDeposit()
const user = accounts[4]
const userBal = await usdtToken.balanceOf(user)
console.log('userBal', userBal.toString())
const senderBal = await usdtToken.balanceOf(sender)
console.log('senderBal', senderBal.toString())
tree.insert(deposit.commitment)
await usdtToken.transfer(user, tokenDenomination, { from: sender })
console.log('transfer done')
const balanceUserBefore = await usdtToken.balanceOf(user)
console.log('balanceUserBefore', balanceUserBefore.toString())
await usdtToken.approve(tornado.address, tokenDenomination, { from: user })
console.log('approve done')
const allowanceUser = await usdtToken.allowance(user, tornado.address)
console.log('allowanceUser', allowanceUser.toString())
await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
console.log('deposit done')
const balanceUserAfter = await usdtToken.balanceOf(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
const { pathElements, pathIndices } = tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer: operator,
recipient,
fee,
refund,
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const balanceTornadoBefore = await usdtToken.balanceOf(tornado.address)
const balanceRelayerBefore = await usdtToken.balanceOf(relayer)
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
const balanceReceiverBefore = await usdtToken.balanceOf(toFixedHex(recipient, 20))
const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
let isSpent = await tornado.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
isSpent.should.be.equal(false)
// Uncomment to measure gas usage
// gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
// console.log('withdraw gas:', gas)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
const balanceTornadoAfter = await usdtToken.balanceOf(tornado.address)
const balanceRelayerAfter = await usdtToken.balanceOf(relayer)
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
const balanceReceiverAfter = await usdtToken.balanceOf(toFixedHex(recipient, 20))
const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
const feeBN = toBN(fee.toString())
balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(tokenDenomination)))
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore).add(feeBN))
balanceReceiverAfter.should.be.eq.BN(toBN(balanceReceiverBefore).add(toBN(tokenDenomination)))
ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore).add(toBN(refund)).sub(feeBN))
logs[0].event.should.be.equal('Withdrawal')
logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString()))
logs[0].args.relayer.should.be.eq.BN(operator)
logs[0].args.fee.should.be.eq.BN(feeBN)
isSpent = await tornado.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
isSpent.should.be.equal(true)
})
it.skip('should work with REAL DAI', async () => {
// dont forget to specify your token in .env
// and send `tokenDenomination` to accounts[0] (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1)
// run ganache as
// npx ganache-cli --fork https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448@13146218 -d --keepAliveTimeout 20
const deposit = generateDeposit()
const user = accounts[4]
const userBal = await token.balanceOf(user)
console.log('userBal', userBal.toString())
const senderBal = await token.balanceOf(sender)
console.log('senderBal', senderBal.toString())
tree.insert(deposit.commitment)
await token.transfer(user, tokenDenomination, { from: sender })
console.log('transfer done')
const balanceUserBefore = await token.balanceOf(user)
console.log('balanceUserBefore', balanceUserBefore.toString())
await token.approve(tornado.address, tokenDenomination, { from: user })
console.log('approve done')
await tornado.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
console.log('deposit done')
const balanceUserAfter = await token.balanceOf(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
const { pathElements, pathIndices } = tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer: operator,
recipient,
fee,
refund,
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const balanceTornadoBefore = await token.balanceOf(tornado.address)
const balanceRelayerBefore = await token.balanceOf(relayer)
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20))
const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
let isSpent = await tornado.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
isSpent.should.be.equal(false)
// Uncomment to measure gas usage
// gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
// console.log('withdraw gas:', gas)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
const { logs } = await tornado.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
console.log('withdraw done')
const balanceTornadoAfter = await token.balanceOf(tornado.address)
const balanceRelayerAfter = await token.balanceOf(relayer)
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
const balanceReceiverAfter = await token.balanceOf(toFixedHex(recipient, 20))
const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
const feeBN = toBN(fee.toString())
balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(tokenDenomination)))
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore).add(feeBN))
balanceReceiverAfter.should.be.eq.BN(toBN(balanceReceiverBefore).add(toBN(tokenDenomination)))
ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore).add(toBN(refund)).sub(feeBN))
logs[0].event.should.be.equal('Withdrawal')
logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString()))
logs[0].args.relayer.should.be.eq.BN(operator)
logs[0].args.fee.should.be.eq.BN(feeBN)
isSpent = await tornado.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
isSpent.should.be.equal(true)
})
})
afterEach(async () => {
await revertSnapshot(snapshotId.result)
// eslint-disable-next-line require-atomic-updates
snapshotId = await takeSnapshot()
tree = new MerkleTree(levels)
})
})

540
test/ETHTornado.test.js Normal file
View File

@ -0,0 +1,540 @@
/* global artifacts, web3, contract */
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
const fs = require('fs')
const { toBN, randomHex } = require('web3-utils')
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
const Tornado = artifacts.require('./ETHTornado.sol')
const { ETH_AMOUNT, MERKLE_TREE_HEIGHT } = process.env
const websnarkUtils = require('websnark/src/utils')
const buildGroth16 = require('websnark/src/groth16')
const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts
const unstringifyBigInts2 = require('snarkjs/src/stringifybigint').unstringifyBigInts
const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt
const crypto = require('crypto')
const circomlib = require('circomlib')
const MerkleTree = require('fixed-merkle-tree')
const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
const toFixedHex = (number, length = 32) =>
'0x' +
bigInt(number)
.toString(16)
.padStart(length * 2, '0')
const getRandomRecipient = () => rbigint(20)
function generateDeposit() {
let deposit = {
secret: rbigint(31),
nullifier: rbigint(31),
}
const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
deposit.commitment = pedersenHash(preimage)
return deposit
}
// eslint-disable-next-line no-unused-vars
function BNArrayToStringArray(array) {
const arrayToPrint = []
array.forEach((item) => {
arrayToPrint.push(item.toString())
})
return arrayToPrint
}
function snarkVerify(proof) {
proof = unstringifyBigInts2(proof)
const verification_key = unstringifyBigInts2(require('../build/circuits/withdraw_verification_key.json'))
return snarkjs['groth'].isValid(verification_key, proof, proof.publicSignals)
}
contract('ETHTornado', (accounts) => {
let tornado
const sender = accounts[0]
const operator = accounts[0]
const levels = MERKLE_TREE_HEIGHT || 16
const value = ETH_AMOUNT || '1000000000000000000' // 1 ether
let snapshotId
let tree
const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
const refund = bigInt(0)
const recipient = getRandomRecipient()
const relayer = accounts[1]
let groth16
let circuit
let proving_key
before(async () => {
tree = new MerkleTree(levels)
tornado = await Tornado.deployed()
snapshotId = await takeSnapshot()
groth16 = await buildGroth16()
circuit = require('../build/circuits/withdraw.json')
proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
})
describe('#constructor', () => {
it('should initialize', async () => {
const etherDenomination = await tornado.denomination()
etherDenomination.should.be.eq.BN(toBN(value))
})
})
describe('#deposit', () => {
it('should emit event', async () => {
let commitment = toFixedHex(42)
let { logs } = await tornado.deposit(commitment, { value, from: sender })
logs[0].event.should.be.equal('Deposit')
logs[0].args.commitment.should.be.equal(commitment)
logs[0].args.leafIndex.should.be.eq.BN(0)
commitment = toFixedHex(12)
;({ logs } = await tornado.deposit(commitment, { value, from: accounts[2] }))
logs[0].event.should.be.equal('Deposit')
logs[0].args.commitment.should.be.equal(commitment)
logs[0].args.leafIndex.should.be.eq.BN(1)
})
it('should throw if there is a such commitment', async () => {
const commitment = toFixedHex(42)
await tornado.deposit(commitment, { value, from: sender }).should.be.fulfilled
const error = await tornado.deposit(commitment, { value, from: sender }).should.be.rejected
error.reason.should.be.equal('The commitment has been submitted')
})
})
describe('snark proof verification on js side', () => {
it('should detect tampering', async () => {
const deposit = generateDeposit()
tree.insert(deposit.commitment)
const { pathElements, pathIndices } = tree.path(0)
const input = stringifyBigInts({
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
let proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const originalProof = JSON.parse(JSON.stringify(proofData))
let result = snarkVerify(proofData)
result.should.be.equal(true)
// nullifier
proofData.publicSignals[1] =
'133792158246920651341275668520530514036799294649489851421007411546007850802'
result = snarkVerify(proofData)
result.should.be.equal(false)
proofData = originalProof
// try to cheat with recipient
proofData.publicSignals[2] = '133738360804642228759657445999390850076318544422'
result = snarkVerify(proofData)
result.should.be.equal(false)
proofData = originalProof
// fee
proofData.publicSignals[3] = '1337100000000000000000'
result = snarkVerify(proofData)
result.should.be.equal(false)
proofData = originalProof
})
})
describe('#withdraw', () => {
it('should work', async () => {
const deposit = generateDeposit()
const user = accounts[4]
tree.insert(deposit.commitment)
const balanceUserBefore = await web3.eth.getBalance(user)
// Uncomment to measure gas usage
// let gas = await tornado.deposit.estimateGas(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
// console.log('deposit gas:', gas)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: user, gasPrice: '0' })
const balanceUserAfter = await web3.eth.getBalance(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(value)))
const { pathElements, pathIndices } = tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer: operator,
recipient,
fee,
refund,
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const balanceTornadoBefore = await web3.eth.getBalance(tornado.address)
const balanceRelayerBefore = await web3.eth.getBalance(relayer)
const balanceOperatorBefore = await web3.eth.getBalance(operator)
const balanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
let isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(false)
// Uncomment to measure gas usage
// gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
// console.log('withdraw gas:', gas)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
const { logs } = await tornado.withdraw(proof, ...args, { from: relayer, gasPrice: '0' })
const balanceTornadoAfter = await web3.eth.getBalance(tornado.address)
const balanceRelayerAfter = await web3.eth.getBalance(relayer)
const balanceOperatorAfter = await web3.eth.getBalance(operator)
const balanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
const feeBN = toBN(fee.toString())
balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(value)))
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
balanceOperatorAfter.should.be.eq.BN(toBN(balanceOperatorBefore).add(feeBN))
balanceReceiverAfter.should.be.eq.BN(toBN(balanceReceiverBefore).add(toBN(value)).sub(feeBN))
logs[0].event.should.be.equal('Withdrawal')
logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash))
logs[0].args.relayer.should.be.eq.BN(operator)
logs[0].args.fee.should.be.eq.BN(feeBN)
isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(true)
})
it('should prevent double spend', async () => {
const deposit = generateDeposit()
tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { pathElements, pathIndices } = tree.path(0)
const input = stringifyBigInts({
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
await tornado.withdraw(proof, ...args, { from: relayer }).should.be.fulfilled
const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('The note has been already spent')
})
it('should prevent double spend with overflow', async () => {
const deposit = generateDeposit()
tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { pathElements, pathIndices } = tree.path(0)
const input = stringifyBigInts({
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const args = [
toFixedHex(input.root),
toFixedHex(
toBN(input.nullifierHash).add(
toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617'),
),
),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('verifier-gte-snark-scalar-field')
})
it('fee should be less or equal transfer value', async () => {
const deposit = generateDeposit()
tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { pathElements, pathIndices } = tree.path(0)
const largeFee = bigInt(value).add(bigInt(1))
const input = stringifyBigInts({
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee: largeFee,
refund,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Fee exceeds transfer value')
})
it('should throw for corrupted merkle tree root', async () => {
const deposit = generateDeposit()
tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { pathElements, pathIndices } = tree.path(0)
const input = stringifyBigInts({
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
root: tree.root(),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const args = [
toFixedHex(randomHex(32)),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Cannot find your merkle root')
})
it('should reject with tampered public inputs', async () => {
const deposit = generateDeposit()
tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
let { pathElements, pathIndices } = tree.path(0)
const input = stringifyBigInts({
root: tree.root(),
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund,
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
let { proof } = websnarkUtils.toSolidityInput(proofData)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
let incorrectArgs
const originalProof = proof.slice()
// recipient
incorrectArgs = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex('0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337', 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
let error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof')
// fee
incorrectArgs = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex('0x000000000000000000000000000000000000000000000000015345785d8a0000'),
toFixedHex(input.refund),
]
error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof')
// nullifier
incorrectArgs = [
toFixedHex(input.root),
toFixedHex('0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad'),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof')
// proof itself
proof = '0xbeef' + proof.substr(6)
await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
// should work with original values
await tornado.withdraw(originalProof, ...args, { from: relayer }).should.be.fulfilled
})
it('should reject with non zero refund', async () => {
const deposit = generateDeposit()
tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { pathElements, pathIndices } = tree.path(0)
const input = stringifyBigInts({
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
root: tree.root(),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund: bigInt(1),
secret: deposit.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Refund value is supposed to be zero for ETH instance')
})
})
describe('#isSpent', () => {
it('should work', async () => {
const deposit1 = generateDeposit()
const deposit2 = generateDeposit()
tree.insert(deposit1.commitment)
tree.insert(deposit2.commitment)
await tornado.deposit(toFixedHex(deposit1.commitment), { value, gasPrice: '0' })
await tornado.deposit(toFixedHex(deposit2.commitment), { value, gasPrice: '0' })
const { pathElements, pathIndices } = tree.path(1)
// Circuit input
const input = stringifyBigInts({
// public
root: tree.root(),
nullifierHash: pedersenHash(deposit2.nullifier.leInt2Buff(31)),
relayer: operator,
recipient,
fee,
refund,
// private
nullifier: deposit2.nullifier,
secret: deposit2.secret,
pathElements: pathElements,
pathIndices: pathIndices,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund),
]
await tornado.withdraw(proof, ...args, { from: relayer, gasPrice: '0' })
const nullifierHash1 = toFixedHex(pedersenHash(deposit1.nullifier.leInt2Buff(31)))
const nullifierHash2 = toFixedHex(pedersenHash(deposit2.nullifier.leInt2Buff(31)))
const spentArray = await tornado.isSpentArray([nullifierHash1, nullifierHash2])
spentArray.should.be.deep.equal([false, true])
})
})
afterEach(async () => {
await revertSnapshot(snapshotId.result)
// eslint-disable-next-line require-atomic-updates
snapshotId = await takeSnapshot()
tree = new MerkleTree(levels)
})
})

View File

@ -1,164 +1,58 @@
/* global artifacts, web3, contract, assert */
require('chai')
.use(require('bn-chai')(web3.utils.BN))
.use(require('chai-as-promised'))
.should()
/* global artifacts, web3, contract */
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper')
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
const MerkleTreeWithHistory = artifacts.require('./MerkleTreeWithHistoryMock.sol')
const MiMC = artifacts.require('./MiMC.sol')
const hasherContract = artifacts.require('./Hasher.sol')
const MerkleTree = require('../lib/MerkleTree')
const MimcHasher = require('../lib/MiMC')
const MerkleTree = require('fixed-merkle-tree')
const { AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env
const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt
const { ETH_AMOUNT, MERKLE_TREE_HEIGHT } = process.env
// eslint-disable-next-line no-unused-vars
function BNArrayToStringArray(array) {
const arrayToPrint = []
array.forEach(item => {
array.forEach((item) => {
arrayToPrint.push(item.toString())
})
return arrayToPrint
}
contract('MerkleTreeWithHistory', accounts => {
function toFixedHex(number, length = 32) {
let str = bigInt(number).toString(16)
while (str.length < length * 2) str = '0' + str
str = '0x' + str
return str
}
contract('MerkleTreeWithHistory', (accounts) => {
let merkleTreeWithHistory
let miMC
let hasherInstance
let levels = MERKLE_TREE_HEIGHT || 16
let zeroValue = EMPTY_ELEMENT || 1337
const sender = accounts[0]
// eslint-disable-next-line no-unused-vars
const value = AMOUNT || '1000000000000000000'
const value = ETH_AMOUNT || '1000000000000000000'
let snapshotId
let prefix = 'test'
let tree
let hasher
before(async () => {
tree = new MerkleTree(
levels,
zeroValue,
null,
prefix,
)
miMC = await MiMC.deployed()
await MerkleTreeWithHistory.link(MiMC, miMC.address)
merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, zeroValue)
tree = new MerkleTree(levels)
hasherInstance = await hasherContract.deployed()
merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, hasherInstance.address)
snapshotId = await takeSnapshot()
})
describe('#constructor', () => {
it('should initialize', async () => {
const filled_subtrees = await merkleTreeWithHistory.filled_subtrees()
filled_subtrees[0].should.be.eq.BN(zeroValue)
const zeros = await merkleTreeWithHistory.zeros()
zeros[0].should.be.eq.BN(zeroValue)
})
})
describe('merkleTreeLib', () => {
it('index_to_key', () => {
assert.equal(
MerkleTree.index_to_key('test', 5, 20),
'test_tree_5_20',
)
})
it('tests insert', async () => {
hasher = new MimcHasher()
tree = new MerkleTree(
2,
zeroValue,
null,
prefix,
)
await tree.insert('5')
let { root, path_elements } = await tree.path(0)
const calculated_root = hasher.hash(null,
hasher.hash(null, '5', path_elements[0]),
path_elements[1]
)
// console.log(root)
assert.equal(root, calculated_root)
})
it('creation odd elements count', async () => {
const elements = [12, 13, 14, 15, 16, 17, 18, 19, 20]
for(const [, el] of Object.entries(elements)) {
await tree.insert(el)
}
const batchTree = new MerkleTree(
levels,
zeroValue,
elements,
prefix,
)
for(const [i] of Object.entries(elements)) {
const pathViaConstructor = await batchTree.path(i)
const pathViaUpdate = await tree.path(i)
pathViaConstructor.should.be.deep.equal(pathViaUpdate)
}
})
it('should find an element', async () => {
const elements = [12, 13, 14, 15, 16, 17, 18, 19, 20]
for(const [, el] of Object.entries(elements)) {
await tree.insert(el)
}
let index = tree.getIndexByElement(13)
index.should.be.equal(1)
index = tree.getIndexByElement(19)
index.should.be.equal(7)
index = tree.getIndexByElement(12)
index.should.be.equal(0)
index = tree.getIndexByElement(20)
index.should.be.equal(8)
index = tree.getIndexByElement(42)
index.should.be.equal(false)
})
it('creation even elements count', async () => {
const elements = [12, 13, 14, 15, 16, 17]
for(const [, el] of Object.entries(elements)) {
await tree.insert(el)
}
const batchTree = new MerkleTree(
levels,
zeroValue,
elements,
prefix,
)
for(const [i] of Object.entries(elements)) {
const pathViaConstructor = await batchTree.path(i)
const pathViaUpdate = await tree.path(i)
pathViaConstructor.should.be.deep.equal(pathViaUpdate)
}
})
it.skip('creation using 30000 elements', () => {
const elements = []
for(let i = 1000; i < 31001; i++) {
elements.push(i)
}
console.time('MerkleTree')
tree = new MerkleTree(
levels,
zeroValue,
elements,
prefix,
)
console.timeEnd('MerkleTree')
// 2,7 GHz Intel Core i7
// 1000 : 1949.084ms
// 10000: 19456.220ms
// 30000: 63406.679ms
const zeroValue = await merkleTreeWithHistory.ZERO_VALUE()
const firstSubtree = await merkleTreeWithHistory.filledSubtrees(0)
firstSubtree.should.be.equal(toFixedHex(zeroValue))
const firstZero = await merkleTreeWithHistory.zeros(0)
firstZero.should.be.equal(toFixedHex(zeroValue))
})
})
@ -167,28 +61,57 @@ contract('MerkleTreeWithHistory', accounts => {
let rootFromContract
for (let i = 1; i < 11; i++) {
await merkleTreeWithHistory.insert(i, { from: sender })
await tree.insert(i)
let { root } = await tree.path(i - 1)
await merkleTreeWithHistory.insert(toFixedHex(i), { from: sender })
tree.insert(i)
rootFromContract = await merkleTreeWithHistory.getLastRoot()
root.should.be.equal(rootFromContract.toString())
toFixedHex(tree.root()).should.be.equal(rootFromContract.toString())
}
})
it('should reject if tree is full', async () => {
levels = 6
zeroValue = 1337
merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, zeroValue)
const levels = 6
const merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, hasherInstance.address)
for (let i = 0; i < 2**(levels - 1); i++) {
await merkleTreeWithHistory.insert(i+42).should.be.fulfilled
for (let i = 0; i < 2 ** levels; i++) {
await merkleTreeWithHistory.insert(toFixedHex(i + 42)).should.be.fulfilled
}
let error = await merkleTreeWithHistory.insert(1337).should.be.rejected
error.reason.should.be.equal('Merkle tree is full')
let error = await merkleTreeWithHistory.insert(toFixedHex(1337)).should.be.rejected
error.reason.should.be.equal('Merkle tree is full. No more leaves can be added')
error = await merkleTreeWithHistory.insert(1).should.be.rejected
error.reason.should.be.equal('Merkle tree is full')
error = await merkleTreeWithHistory.insert(toFixedHex(1)).should.be.rejected
error.reason.should.be.equal('Merkle tree is full. No more leaves can be added')
})
it.skip('hasher gas', async () => {
const levels = 6
const merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels)
const zeroValue = await merkleTreeWithHistory.zeroValue()
const gas = await merkleTreeWithHistory.hashLeftRight.estimateGas(zeroValue, zeroValue)
console.log('gas', gas - 21000)
})
})
describe('#isKnownRoot', () => {
it('should work', async () => {
for (let i = 1; i < 5; i++) {
await merkleTreeWithHistory.insert(toFixedHex(i), { from: sender }).should.be.fulfilled
await tree.insert(i)
let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(tree.root()))
isKnown.should.be.equal(true)
}
await merkleTreeWithHistory.insert(toFixedHex(42), { from: sender }).should.be.fulfilled
// check outdated root
let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(tree.root()))
isKnown.should.be.equal(true)
})
it('should not return uninitialized roots', async () => {
await merkleTreeWithHistory.insert(toFixedHex(42), { from: sender }).should.be.fulfilled
let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(0))
isKnown.should.be.equal(false)
})
})
@ -196,14 +119,6 @@ contract('MerkleTreeWithHistory', accounts => {
await revertSnapshot(snapshotId.result)
// eslint-disable-next-line require-atomic-updates
snapshotId = await takeSnapshot()
hasher = new MimcHasher()
tree = new MerkleTree(
levels,
zeroValue,
null,
prefix,
null,
hasher,
)
tree = new MerkleTree(levels)
})
})

View File

@ -1,427 +0,0 @@
/* global artifacts, web3, contract */
require('chai')
.use(require('bn-chai')(web3.utils.BN))
.use(require('chai-as-promised'))
.should()
const fs = require('fs')
const { toBN, toHex, randomHex } = require('web3-utils')
const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper')
const Mixer = artifacts.require('./Mixer.sol')
const { AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env
const websnarkUtils = require('websnark/src/utils')
const buildGroth16 = require('websnark/src/groth16')
const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts
const unstringifyBigInts2 = require('snarkjs/src/stringifybigint').unstringifyBigInts
const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt
const crypto = require('crypto')
const circomlib = require('circomlib')
const MerkleTree = require('../lib/MerkleTree')
const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
function generateDeposit() {
let deposit = {
secret: rbigint(31),
nullifier: rbigint(31),
}
const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
deposit.commitment = pedersenHash(preimage)
return deposit
}
// eslint-disable-next-line no-unused-vars
function BNArrayToStringArray(array) {
const arrayToPrint = []
array.forEach(item => {
arrayToPrint.push(item.toString())
})
return arrayToPrint
}
function getRandomReceiver() {
let receiver = rbigint(20)
while (toHex(receiver.toString()).length !== 42) {
receiver = rbigint(20)
}
return receiver
}
function snarkVerify(proof) {
proof = unstringifyBigInts2(websnarkUtils.fromSolidityInput(proof))
const verification_key = unstringifyBigInts2(require('../build/circuits/withdraw_verification_key.json'))
return snarkjs['groth'].isValid(verification_key, proof, proof.publicSignals)
}
contract('Mixer', accounts => {
let mixer
const sender = accounts[0]
const operator = accounts[0]
const levels = MERKLE_TREE_HEIGHT || 16
const zeroValue = EMPTY_ELEMENT || 1337
const value = AMOUNT || '1000000000000000000' // 1 ether
let snapshotId
let prefix = 'test'
let tree
const fee = bigInt(AMOUNT).shr(1) || bigInt(1e17)
const receiver = getRandomReceiver()
const relayer = accounts[1]
let groth16
let circuit
let proving_key
before(async () => {
tree = new MerkleTree(
levels,
zeroValue,
null,
prefix,
)
mixer = await Mixer.deployed()
snapshotId = await takeSnapshot()
groth16 = await buildGroth16()
circuit = require('../build/circuits/withdraw.json')
proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
})
describe('#constructor', () => {
it('should initialize', async () => {
const transferValue = await mixer.transferValue()
transferValue.should.be.eq.BN(toBN(value))
})
})
describe('#deposit', () => {
it('should emit event', async () => {
let commitment = 42
let { logs } = await mixer.deposit(commitment, { value, from: sender })
logs[0].event.should.be.equal('Deposit')
logs[0].args.commitment.should.be.eq.BN(toBN(commitment))
logs[0].args.leafIndex.should.be.eq.BN(toBN(0))
commitment = 12;
({ logs } = await mixer.deposit(commitment, { value, from: accounts[2] }))
logs[0].event.should.be.equal('Deposit')
logs[0].args.commitment.should.be.eq.BN(toBN(commitment))
logs[0].args.leafIndex.should.be.eq.BN(toBN(1))
})
it('should not deposit if disabled', async () => {
let commitment = 42;
(await mixer.isDepositsEnabled()).should.be.equal(true)
const err = await mixer.toggleDeposits({ from: accounts[1] }).should.be.rejected
err.reason.should.be.equal('unauthorized')
await mixer.toggleDeposits({ from: sender });
(await mixer.isDepositsEnabled()).should.be.equal(false)
let error = await mixer.deposit(commitment, { value, from: sender }).should.be.rejected
error.reason.should.be.equal('deposits disabled')
})
it('should throw if there is a such commitment', async () => {
const commitment = 42
await mixer.deposit(commitment, { value, from: sender }).should.be.fulfilled
const error = await mixer.deposit(commitment, { value, from: sender }).should.be.rejected
error.reason.should.be.equal('The commitment has been submitted')
})
})
describe('snark proof verification on js side', () => {
it('should detect tampering', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
const { root, path_elements, path_index } = await tree.path(0)
const input = stringifyBigInts({
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
receiver,
fee,
secret: deposit.secret,
pathElements: path_elements,
pathIndex: path_index,
})
let proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const originalProof = JSON.parse(JSON.stringify(proof))
let result = snarkVerify(proof)
result.should.be.equal(true)
// nullifier
proof.publicSignals[1] = '133792158246920651341275668520530514036799294649489851421007411546007850802'
result = snarkVerify(proof)
result.should.be.equal(false)
proof = originalProof
// try to cheat with recipient
proof.publicSignals[2] = '133738360804642228759657445999390850076318544422'
result = snarkVerify(proof)
result.should.be.equal(false)
proof = originalProof
// fee
proof.publicSignals[3] = '1337100000000000000000'
result = snarkVerify(proof)
result.should.be.equal(false)
proof = originalProof
})
})
describe('#withdraw', () => {
it('should work', async () => {
const deposit = generateDeposit()
const user = accounts[4]
await tree.insert(deposit.commitment)
const balanceUserBefore = await web3.eth.getBalance(user)
// Uncomment to measure gas usage
// let gas = await mixer.deposit.estimateGas(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
// console.log('deposit gas:', gas)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
const balanceUserAfter = await web3.eth.getBalance(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(value)))
const { root, path_elements, path_index } = await tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
receiver,
fee,
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndex: path_index,
})
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
const balanceMixerBefore = await web3.eth.getBalance(mixer.address)
const balanceRelayerBefore = await web3.eth.getBalance(relayer)
const balanceOperatorBefore = await web3.eth.getBalance(operator)
const balanceRecieverBefore = await web3.eth.getBalance(toHex(receiver.toString()))
let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
isSpent.should.be.equal(false)
// Uncomment to measure gas usage
// gas = await mixer.withdraw.estimateGas(pi_a, pi_b, pi_c, publicSignals, { from: relayer, gasPrice: '0' })
// console.log('withdraw gas:', gas)
const { logs } = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer, gasPrice: '0' })
const balanceMixerAfter = await web3.eth.getBalance(mixer.address)
const balanceRelayerAfter = await web3.eth.getBalance(relayer)
const balanceOperatorAfter = await web3.eth.getBalance(operator)
const balanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString()))
const feeBN = toBN(fee.toString())
balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(value)))
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
balanceOperatorAfter.should.be.eq.BN(toBN(balanceOperatorBefore).add(feeBN))
balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(value)).sub(feeBN))
logs[0].event.should.be.equal('Withdraw')
logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString()))
logs[0].args.fee.should.be.eq.BN(feeBN)
isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
isSpent.should.be.equal(true)
})
it('should prevent double spend', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const input = stringifyBigInts({
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
receiver,
fee,
secret: deposit.secret,
pathElements: path_elements,
pathIndex: path_index,
})
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.fulfilled
const error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
error.reason.should.be.equal('The note has been already spent')
})
it('should prevent double spend with overflow', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const input = stringifyBigInts({
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
receiver,
fee,
secret: deposit.secret,
pathElements: path_elements,
pathIndex: path_index,
})
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
publicSignals[1] ='0x' + toBN(publicSignals[1]).add(toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617')).toString('hex')
const error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
error.reason.should.be.equal('verifier-gte-snark-scalar-field')
})
it('fee should be less or equal transfer value', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const oneEtherFee = bigInt(1e18) // 1 ether
const input = stringifyBigInts({
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
receiver,
fee: oneEtherFee,
secret: deposit.secret,
pathElements: path_elements,
pathIndex: path_index,
})
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
const error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Fee exceeds transfer value')
})
it('should throw for corrupted merkle tree root', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const input = stringifyBigInts({
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
root,
nullifier: deposit.nullifier,
receiver,
fee,
secret: deposit.secret,
pathElements: path_elements,
pathIndex: path_index,
})
const dummyRoot = randomHex(32)
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
publicSignals[0] = dummyRoot
const error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Cannot find your merkle root')
})
it('should reject with tampered public inputs', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
let { root, path_elements, path_index } = await tree.path(0)
const input = stringifyBigInts({
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
receiver,
fee,
secret: deposit.secret,
pathElements: path_elements,
pathIndex: path_index,
})
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
let { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
const originalPublicSignals = publicSignals.slice()
const originalPi_a = pi_a.slice()
// receiver
publicSignals[2] = '0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337'
let error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof')
// fee
publicSignals = originalPublicSignals.slice()
publicSignals[3] = '0x000000000000000000000000000000000000000000000000015345785d8a0000'
error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof')
// nullifier
publicSignals = originalPublicSignals.slice()
publicSignals[1] = '0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad'
error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof')
// proof itself
pi_a[0] = '0x261d81d8203437f29b38a88c4263476d858e6d9645cf21740461684412b31337'
await mixer.withdraw(pi_a, pi_b, pi_c, originalPublicSignals, { from: relayer }).should.be.rejected
// should work with original values
await mixer.withdraw(originalPi_a, pi_b, pi_c, originalPublicSignals, { from: relayer }).should.be.fulfilled
})
})
describe('#changeOperator', () => {
it('should work', async () => {
let operator = await mixer.operator()
operator.should.be.equal(sender)
const newOperator = accounts[7]
await mixer.changeOperator(newOperator).should.be.fulfilled
operator = await mixer.operator()
operator.should.be.equal(newOperator)
})
it('cannot change from different address', async () => {
let operator = await mixer.operator()
operator.should.be.equal(sender)
const newOperator = accounts[7]
const error = await mixer.changeOperator(newOperator, { from: accounts[7] }).should.be.rejected
error.reason.should.be.equal('unauthorized')
})
})
afterEach(async () => {
await revertSnapshot(snapshotId.result)
// eslint-disable-next-line require-atomic-updates
snapshotId = await takeSnapshot()
tree = new MerkleTree(
levels,
zeroValue,
null,
prefix,
)
})
})

View File

@ -1,10 +1,6 @@
require('dotenv').config()
const HDWalletProvider = require('truffle-hdwallet-provider')
const HDWalletProvider = require('@truffle/hdwallet-provider')
const utils = require('web3-utils')
// const infuraKey = "fj4jll3k.....";
//
// const fs = require('fs');
// const mnemonic = fs.readFileSync(".secret").toString().trim();
module.exports = {
/**
@ -18,38 +14,49 @@ module.exports = {
*/
networks: {
// Useful for testing. The `development` name is special - truffle uses it by default
// if it's defined here and no other network is specified at the command line.
// You should run a client (like ganache-cli, geth or parity) in a separate terminal
// tab if you use this network and you must also set the `host`, `port` and `network_id`
// options below to some value.
development: {
host: '127.0.0.1', // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: '*', // Any network (default: none)
host: '127.0.0.1', // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: '*', // Any network (default: none)
},
// Another network with more advanced options...
// advanced: {
// port: 8777, // Custom port
// network_id: 1342, // Custom network
// gas: 8500000, // Gas sent with each transaction (default: ~6700000)
// gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei)
// from: <address>, // Account to send txs from (default: accounts[0])
// websockets: true // Enable EventEmitter interface for web3 (default: false)
// },
// Useful for deploying to a public network.
// NB: It's important to wrap the provider as a function.
kovan: {
provider: () => new HDWalletProvider(process.env.PRIVATE_KEY, 'https://kovan.infura.io/v3/c7463beadf2144e68646ff049917b716'),
provider: () =>
new HDWalletProvider(
process.env.PRIVATE_KEY,
'https://kovan.infura.io/v3/97c8bf358b9942a9853fab1ba93dc5b3',
),
network_id: 42,
gas: 7000000,
gas: 6000000,
gasPrice: utils.toWei('1', 'gwei'),
// confirmations: 0,
// timeoutBlocks: 200,
skipDryRun: true
skipDryRun: true,
},
goerli: {
provider: () =>
new HDWalletProvider(
process.env.PRIVATE_KEY,
'https://goerli.infura.io/v3/d34c08f2cb7c4111b645d06ac7e35ba8',
),
network_id: 5,
gas: 6000000,
gasPrice: utils.toWei('1', 'gwei'),
// confirmations: 0,
// timeoutBlocks: 200,
skipDryRun: true,
},
rinkeby: {
provider: () =>
new HDWalletProvider(
process.env.PRIVATE_KEY,
'https://rinkeby.infura.io/v3/97c8bf358b9942a9853fab1ba93dc5b3',
),
network_id: 4,
gas: 6000000,
gasPrice: utils.toWei('1', 'gwei'),
// confirmations: 0,
// timeoutBlocks: 200,
skipDryRun: true,
},
mainnet: {
provider: () => new HDWalletProvider(process.env.PRIVATE_KEY, 'http://ethereum-rpc.trustwalletapp.com'),
@ -58,18 +65,10 @@ module.exports = {
gasPrice: utils.toWei('2', 'gwei'),
// confirmations: 0,
// timeoutBlocks: 200,
skipDryRun: true
skipDryRun: true,
},
// Useful for private networks
// private: {
// provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
// network_id: 2111, // This network is yours, in the cloud.
// production: true // Treats this network as if it was a public net. (default: false)
// }
},
// Set default mocha options here, use special reporters etc.
mocha: {
// timeout: 100000
},
@ -77,15 +76,23 @@ module.exports = {
// Configure your compilers
compilers: {
solc: {
version: '0.5.10', // Fetch exact version from solc-bin (default: truffle's version)
// docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
settings: { // See the solidity docs for advice about optimization and evmVersion
version: '0.7.6',
settings: {
optimizer: {
enabled: true,
runs: 200
runs: 200,
},
// evmVersion: "byzantium"
}
}
}
},
},
external: {
command: 'node ./scripts/compileHasher.js',
targets: [
{
path: './build/Hasher.json',
},
],
},
},
plugins: ['solidity-coverage'],
}

13191
yarn.lock Normal file

File diff suppressed because it is too large Load Diff