From b8142d03bb1626daa851f3ab7d3fe99dd56327bc Mon Sep 17 00:00:00 2001 From: Alexey Date: Tue, 20 Aug 2019 23:39:21 +0300 Subject: [PATCH 01/18] erc20 mixer support WIP --- .env.example | 1 + contracts/ERC20Mixer.sol | 53 ++++++++ contracts/ETHMixer.sol | 47 +++++++ contracts/Mixer.sol | 15 +-- contracts/Mocks/ERC20Mock.sol | 10 ++ ..._deploy_mixer.js => 4_deploy_eth_mixer.js} | 8 +- migrations/5_deploy_erc20_mixer.js | 31 +++++ package-lock.json | 5 + package.json | 1 + test/ERC20Mixer.test.js | 126 ++++++++++++++++++ test/{Mixer.test.js => ETHMixer.test.js} | 2 +- 11 files changed, 282 insertions(+), 17 deletions(-) create mode 100644 contracts/ERC20Mixer.sol create mode 100644 contracts/ETHMixer.sol create mode 100644 contracts/Mocks/ERC20Mock.sol rename migrations/{4_deploy_mixer.js => 4_deploy_eth_mixer.js} (60%) create mode 100644 migrations/5_deploy_erc20_mixer.js create mode 100644 test/ERC20Mixer.test.js rename test/{Mixer.test.js => ETHMixer.test.js} (99%) diff --git a/.env.example b/.env.example index c008616..103cc28 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,4 @@ MERKLE_TREE_HEIGHT=16 AMOUNT=1000000000000000000 EMPTY_ELEMENT=1337 PRIVATE_KEY= +ERC20_TOKEN= diff --git a/contracts/ERC20Mixer.sol b/contracts/ERC20Mixer.sol new file mode 100644 index 0000000..8457b1f --- /dev/null +++ b/contracts/ERC20Mixer.sol @@ -0,0 +1,53 @@ +// 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 "./Mixer.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract ERC20Mixer is Mixer { + IERC20 public token; + + constructor( + address _verifier, + uint256 _transferValue, + uint8 _merkleTreeHeight, + uint256 _emptyElement, + address payable _operator, + IERC20 _token + ) Mixer(_verifier, _transferValue, _merkleTreeHeight, _emptyElement, _operator) public { + token = _token; + } + + function deposit(uint256 commitment) public { + require(token.transferFrom(msg.sender, address(this), transferValue), "Approve before using"); + _deposit(commitment); + + emit Deposit(commitment, next_index - 1, block.timestamp); + } + + function withdraw(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[4] memory input) public { + _withdraw(a, b, c, input); + address receiver = address(input[2]); + uint256 fee = input[3]; + uint256 nullifierHash = input[1]; + + require(fee < transferValue, "Fee exceeds transfer value"); + token.transfer(receiver, transferValue - fee); + + if (fee > 0) { + token.transfer(operator, fee); + } + + emit Withdraw(receiver, nullifierHash, fee); + } +} diff --git a/contracts/ETHMixer.sol b/contracts/ETHMixer.sol new file mode 100644 index 0000000..d681ac9 --- /dev/null +++ b/contracts/ETHMixer.sol @@ -0,0 +1,47 @@ +// 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 "./Mixer.sol"; + +contract ETHMixer is Mixer { + + constructor( + address _verifier, + uint256 _transferValue, + uint8 _merkleTreeHeight, + uint256 _emptyElement, + address payable _operator + ) Mixer(_verifier, _transferValue, _merkleTreeHeight, _emptyElement, _operator) public {} + + function deposit(uint256 commitment) public payable { + require(msg.value == transferValue, "Please send `transferValue` ETH along with transaction"); + _deposit(commitment); + + emit Deposit(commitment, next_index - 1, block.timestamp); + } + + function withdraw(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[4] memory input) public { + _withdraw(a, b, c, input); + address payable receiver = address(input[2]); + uint256 fee = input[3]; + uint256 nullifierHash = input[1]; + + require(fee < transferValue, "Fee exceeds transfer value"); + receiver.transfer(transferValue - fee); + if (fee > 0) { + operator.transfer(fee); + } + + emit Withdraw(receiver, nullifierHash, fee); + } +} diff --git a/contracts/Mixer.sol b/contracts/Mixer.sol index 9a114af..61942a2 100644 --- a/contracts/Mixer.sol +++ b/contracts/Mixer.sol @@ -52,13 +52,11 @@ contract Mixer is MerkleTreeWithHistory { @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 { + function _deposit(uint256 commitment) internal { 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); } /** @@ -69,23 +67,16 @@ contract Mixer is MerkleTreeWithHistory { - 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 { + function _withdraw(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[4] memory input) internal { 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 { diff --git a/contracts/Mocks/ERC20Mock.sol b/contracts/Mocks/ERC20Mock.sol new file mode 100644 index 0000000..77aad8c --- /dev/null +++ b/contracts/Mocks/ERC20Mock.sol @@ -0,0 +1,10 @@ +pragma solidity ^0.5.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20Mintable.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol"; + +contract ERC20Mock is ERC20Detailed, ERC20Mintable { + constructor() ERC20Detailed("DAIMock", "DAIM", 18) public { + } +} diff --git a/migrations/4_deploy_mixer.js b/migrations/4_deploy_eth_mixer.js similarity index 60% rename from migrations/4_deploy_mixer.js rename to migrations/4_deploy_eth_mixer.js index a1345c7..d529ac6 100644 --- a/migrations/4_deploy_mixer.js +++ b/migrations/4_deploy_eth_mixer.js @@ -1,6 +1,6 @@ /* global artifacts */ require('dotenv').config({ path: '../.env' }) -const Mixer = artifacts.require('Mixer') +const ETHMixer = artifacts.require('ETHMixer') const Verifier = artifacts.require('Verifier') const MiMC = artifacts.require('MiMC') @@ -10,8 +10,8 @@ module.exports = function(deployer, network, accounts) { 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) + await ETHMixer.link(MiMC, miMC.address) + const mixer = await deployer.deploy(ETHMixer, verifier.address, AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, accounts[0]) + console.log('ETHMixer\'s address ', mixer.address) }) } diff --git a/migrations/5_deploy_erc20_mixer.js b/migrations/5_deploy_erc20_mixer.js new file mode 100644 index 0000000..27d13f7 --- /dev/null +++ b/migrations/5_deploy_erc20_mixer.js @@ -0,0 +1,31 @@ +/* global artifacts */ +require('dotenv').config({ path: '../.env' }) +const ERC20Mixer = artifacts.require('ERC20Mixer') +const Verifier = artifacts.require('Verifier') +const MiMC = artifacts.require('MiMC') +const ERC20Mock = artifacts.require('ERC20Mock') + + +module.exports = function(deployer, network, accounts) { + return deployer.then(async () => { + const { MERKLE_TREE_HEIGHT, AMOUNT, EMPTY_ELEMENT, ERC20_TOKEN } = process.env + const verifier = await Verifier.deployed() + const miMC = await MiMC.deployed() + await ERC20Mixer.link(MiMC, miMC.address) + let token = ERC20_TOKEN + if(deployer.network !== 'mainnet') { + const tokenInstance = await deployer.deploy(ERC20Mock) + token = tokenInstance.address + } + const mixer = await deployer.deploy( + ERC20Mixer, + verifier.address, + AMOUNT, + MERKLE_TREE_HEIGHT, + EMPTY_ELEMENT, + accounts[0], + token + ) + console.log('ERC20Mixer\'s address ', mixer.address) + }) +} diff --git a/package-lock.json b/package-lock.json index 429b085..aa2d8c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,11 @@ "regenerator-runtime": "^0.13.2" } }, + "@openzeppelin/contracts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-2.3.0.tgz", + "integrity": "sha512-lf8C3oULQAnsu3OTRP4tP5/ddfil6l65Lg3JQCwAIgc99vZ1jz5qeBoETGGGmczxt+bIyMI06WPP2apC74EZag==" + }, "@resolver-engine/core": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@resolver-engine/core/-/core-0.2.1.tgz", diff --git a/package.json b/package.json index 86c326f..49b8f38 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "author": "", "license": "ISC", "dependencies": { + "@openzeppelin/contracts": "^2.3.0", "bn-chai": "^1.0.1", "browserify": "^16.3.0", "chai": "^4.2.0", diff --git a/test/ERC20Mixer.test.js b/test/ERC20Mixer.test.js new file mode 100644 index 0000000..8bd3714 --- /dev/null +++ b/test/ERC20Mixer.test.js @@ -0,0 +1,126 @@ +/* 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('./ERC20Mixer.sol') +const Token = artifacts.require('./ERC20Mock.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 + let token + 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() + token = await Token.deployed() + token.mint(sender, value) + 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 mixer.token() + tokenFromContract.should.be.equal(token.address) + }) + }) + + describe('#deposit', () => { + it.only('should work', async () => { + const commitment = 43 + await token.approve(mixer.address, value) + + let { logs } = await mixer.deposit(commitment, { 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)) + }) + }) + + afterEach(async () => { + await revertSnapshot(snapshotId.result) + // eslint-disable-next-line require-atomic-updates + snapshotId = await takeSnapshot() + tree = new MerkleTree( + levels, + zeroValue, + null, + prefix, + ) + }) +}) diff --git a/test/Mixer.test.js b/test/ETHMixer.test.js similarity index 99% rename from test/Mixer.test.js rename to test/ETHMixer.test.js index 1d18520..726abe1 100644 --- a/test/Mixer.test.js +++ b/test/ETHMixer.test.js @@ -8,7 +8,7 @@ const fs = require('fs') const { toBN, toHex, randomHex } = require('web3-utils') const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper') -const Mixer = artifacts.require('./Mixer.sol') +const Mixer = artifacts.require('./ETHMixer.sol') const { AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env const websnarkUtils = require('websnark/src/utils') From 0f5a5df522f8966297f52e00a2d2bb8e559fc505 Mon Sep 17 00:00:00 2001 From: Alexey Date: Sat, 24 Aug 2019 13:18:52 +0300 Subject: [PATCH 02/18] withdraw test --- test/ERC20Mixer.test.js | 73 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/test/ERC20Mixer.test.js b/test/ERC20Mixer.test.js index 8bd3714..3a561d5 100644 --- a/test/ERC20Mixer.test.js +++ b/test/ERC20Mixer.test.js @@ -85,7 +85,7 @@ contract('Mixer', accounts => { ) mixer = await Mixer.deployed() token = await Token.deployed() - token.mint(sender, value) + await token.mint(sender, value) snapshotId = await takeSnapshot() groth16 = await buildGroth16() circuit = require('../build/circuits/withdraw.json') @@ -100,7 +100,7 @@ contract('Mixer', accounts => { }) describe('#deposit', () => { - it.only('should work', async () => { + it('should work', async () => { const commitment = 43 await token.approve(mixer.address, value) @@ -112,6 +112,75 @@ contract('Mixer', accounts => { }) }) + describe('#withdraw', () => { + it('should work', async () => { + const deposit = generateDeposit() + const user = accounts[4] + await tree.insert(deposit.commitment) + await token.mint(user, value) + + const balanceUserBefore = await token.balanceOf(user) + await token.approve(mixer.address, value, { from: 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()), { from: user, gasPrice: '0' }) + + const balanceUserAfter = await token.balanceOf(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 token.balanceOf(mixer.address) + const balanceRelayerBefore = await token.balanceOf(relayer) + const balanceOperatorBefore = await token.balanceOf(operator) + const balanceRecieverBefore = await token.balanceOf(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 token.balanceOf(mixer.address) + const balanceRelayerAfter = await token.balanceOf(relayer) + const balanceOperatorAfter = await token.balanceOf(operator) + const balanceRecieverAfter = await token.balanceOf(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) + }) + }) + afterEach(async () => { await revertSnapshot(snapshotId.result) // eslint-disable-next-line require-atomic-updates From 9f33aadd9d264bef70a84c2d0f349f2f167cc7ba Mon Sep 17 00:00:00 2001 From: Alexey Date: Tue, 27 Aug 2019 23:42:24 +0300 Subject: [PATCH 03/18] additional eth for the recipient --- .env.example | 3 +- .gitignore | 2 ++ README.md | 13 +++++++ cli.js | 8 ++--- contracts/ERC20Mixer.sol | 41 ++++++++++++++++++----- contracts/ETHMixer.sol | 25 +++++++++++--- contracts/Mixer.sol | 19 ++--------- migrations/4_deploy_eth_mixer.js | 4 +-- migrations/5_deploy_erc20_mixer.js | 7 ++-- package.json | 2 +- test/ERC20Mixer.test.js | 54 ++++++++++++------------------ test/ETHMixer.test.js | 12 +++---- test/MerkleTreeWithHistory.test.js | 4 +-- truffle-config.js | 2 +- 14 files changed, 113 insertions(+), 83 deletions(-) diff --git a/.env.example b/.env.example index 103cc28..c5c296e 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ MERKLE_TREE_HEIGHT=16 # in wei -AMOUNT=1000000000000000000 +ETH_AMOUNT=100000000000000000 +TOKEN_AMOUNT=100000000000000000 EMPTY_ELEMENT=1337 PRIVATE_KEY= ERC20_TOKEN= diff --git a/.gitignore b/.gitignore index 1de2d9c..135f8a8 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,5 @@ typings/ # DynamoDB Local files .dynamodb/ +ERC20Mixer_flat.sol +ETHMixer_flat.sol diff --git a/README.md b/README.md index e48873e..b1662a0 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,19 @@ spent since it has the same nullifier and it will prevent you from withdrawing y 1. `npx http-server` - serve current dir, you can use any other http server 1. Open `localhost:8080` +## Deploy ETH Tornado Cash +1. `cp .env.example .env` +1. Tune all necessary params +1. `npx truffle migrate --f 2 --to 4` + +## Deploy ERC20 Tornado Cash +1. `cp .env.example .env` +1. Tune all necessary params +1. `npx truffle migrate --f 2 --to 3` +1. `npx truffle migrate --f 5` + +**Note**. If you want to reuse the same verifier for all the mixers, then after you deployed one of the mixers you should only run 4th or 5th migration for ETH or ERC20 mixers respectively (`--f 4 --to 4` or `--f 5`). + ## Credits Special thanks to @barryWhiteHat and @kobigurk for valuable input, diff --git a/cli.js b/cli.js index 8296e48..f5f604c 100755 --- a/cli.js +++ b/cli.js @@ -13,7 +13,7 @@ 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 +let MERKLE_TREE_HEIGHT, ETH_AMOUNT, EMPTY_ELEMENT const inBrowser = (typeof window !== 'undefined') const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes)) @@ -30,7 +30,7 @@ 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 }) + await mixer.methods.deposit('0x' + deposit.commitment.toString(16)).send({ value: ETH_AMOUNT, from: (await web3.eth.getAccounts())[0], gas:1e6 }) const note = '0x' + deposit.preimage.toString('hex') console.log('Your note:', note) @@ -103,7 +103,7 @@ async function init() { 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 + ETH_AMOUNT = 1e18 EMPTY_ELEMENT = 0 } else { web3 = new Web3('http://localhost:8545', null, { transactionConfirmationBlocks: 1 }) @@ -112,7 +112,7 @@ async function init() { 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 + ETH_AMOUNT = process.env.ETH_AMOUNT EMPTY_ELEMENT = process.env.EMPTY_ELEMENT } groth16 = await buildGroth16() diff --git a/contracts/ERC20Mixer.sol b/contracts/ERC20Mixer.sol index 8457b1f..aedbcea 100644 --- a/contracts/ERC20Mixer.sol +++ b/contracts/ERC20Mixer.sol @@ -16,38 +16,61 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract ERC20Mixer is Mixer { IERC20 public token; + // mixed token amount + uint256 public tokenDenomination; + // ether value to cover network fee (for relayer) and to have some ETH on a brand new address + uint256 public etherFeeDenomination; constructor( address _verifier, - uint256 _transferValue, + uint256 _etherFeeDenomination, uint8 _merkleTreeHeight, uint256 _emptyElement, address payable _operator, - IERC20 _token - ) Mixer(_verifier, _transferValue, _merkleTreeHeight, _emptyElement, _operator) public { + IERC20 _token, + uint256 _tokenDenomination + ) Mixer(_verifier, _merkleTreeHeight, _emptyElement, _operator) public { token = _token; + tokenDenomination = _tokenDenomination; + etherFeeDenomination = _etherFeeDenomination; } - function deposit(uint256 commitment) public { - require(token.transferFrom(msg.sender, address(this), transferValue), "Approve before using"); + /** + @dev Deposit funds into the mixer. The caller must send ETH value equal to `etherFeeDenomination` of this mixer. + The caller also has to have at least `tokenDenomination` amount approved for the mixer. + @param commitment the note commitment, which is PedersenHash(nullifier + secret) + */ + function deposit(uint256 commitment) public payable { + require(msg.value == etherFeeDenomination, "Please send `etherFeeDenomination` ETH along with transaction"); + require(token.transferFrom(msg.sender, address(this), tokenDenomination), "Approve before using"); _deposit(commitment); 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 { _withdraw(a, b, c, input); - address receiver = address(input[2]); + address payable receiver = address(input[2]); uint256 fee = input[3]; uint256 nullifierHash = input[1]; - require(fee < transferValue, "Fee exceeds transfer value"); - token.transfer(receiver, transferValue - fee); + require(fee < etherFeeDenomination, "Fee exceeds transfer value"); + receiver.transfer(etherFeeDenomination - fee); if (fee > 0) { - token.transfer(operator, fee); + operator.transfer(fee); } + token.transfer(receiver, tokenDenomination); + emit Withdraw(receiver, nullifierHash, fee); } } diff --git a/contracts/ETHMixer.sol b/contracts/ETHMixer.sol index d681ac9..f194740 100644 --- a/contracts/ETHMixer.sol +++ b/contracts/ETHMixer.sol @@ -14,30 +14,45 @@ pragma solidity ^0.5.8; import "./Mixer.sol"; contract ETHMixer is Mixer { + uint256 public etherDenomination; constructor( address _verifier, - uint256 _transferValue, + uint256 _etherDenomination, uint8 _merkleTreeHeight, uint256 _emptyElement, address payable _operator - ) Mixer(_verifier, _transferValue, _merkleTreeHeight, _emptyElement, _operator) public {} + ) Mixer(_verifier, _merkleTreeHeight, _emptyElement, _operator) public { + etherDenomination = _etherDenomination; + } + /** + @dev Deposit funds into mixer. The caller must send value equal to `etherDenomination` of this mixer. + @param commitment the note commitment, which is PedersenHash(nullifier + secret) + */ function deposit(uint256 commitment) public payable { - require(msg.value == transferValue, "Please send `transferValue` ETH along with transaction"); + require(msg.value == etherDenomination, "Please send `etherDenomination` ETH along with transaction"); _deposit(commitment); 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 { _withdraw(a, b, c, input); address payable receiver = address(input[2]); uint256 fee = input[3]; uint256 nullifierHash = input[1]; - require(fee < transferValue, "Fee exceeds transfer value"); - receiver.transfer(transferValue - fee); + require(fee < etherDenomination, "Fee exceeds transfer value"); + receiver.transfer(etherDenomination - fee); if (fee > 0) { operator.transfer(fee); } diff --git a/contracts/Mixer.sol b/contracts/Mixer.sol index 61942a2..653167a 100644 --- a/contracts/Mixer.sol +++ b/contracts/Mixer.sol @@ -18,7 +18,6 @@ contract IVerifier { } 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 @@ -34,24 +33,20 @@ contract Mixer is MerkleTreeWithHistory { /** @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 + @param _merkleTreeHeight the height of deposits' Merkle Tree + @param _emptyElement default element of the deposits' Merkle Tree + @param _operator operator address (see operator above) */ 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) internal { require(isDepositsEnabled, "deposits disabled"); require(!commitments[commitment], "The commitment has been submitted"); @@ -59,14 +54,6 @@ contract Mixer is MerkleTreeWithHistory { commitments[commitment] = true; } - /** - @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) internal { uint256 root = input[0]; uint256 nullifierHash = input[1]; diff --git a/migrations/4_deploy_eth_mixer.js b/migrations/4_deploy_eth_mixer.js index d529ac6..3626019 100644 --- a/migrations/4_deploy_eth_mixer.js +++ b/migrations/4_deploy_eth_mixer.js @@ -7,11 +7,11 @@ 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 { MERKLE_TREE_HEIGHT, ETH_AMOUNT, EMPTY_ELEMENT } = process.env const verifier = await Verifier.deployed() const miMC = await MiMC.deployed() await ETHMixer.link(MiMC, miMC.address) - const mixer = await deployer.deploy(ETHMixer, verifier.address, AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, accounts[0]) + const mixer = await deployer.deploy(ETHMixer, verifier.address, ETH_AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, accounts[0]) console.log('ETHMixer\'s address ', mixer.address) }) } diff --git a/migrations/5_deploy_erc20_mixer.js b/migrations/5_deploy_erc20_mixer.js index 27d13f7..b710d9e 100644 --- a/migrations/5_deploy_erc20_mixer.js +++ b/migrations/5_deploy_erc20_mixer.js @@ -8,7 +8,7 @@ const ERC20Mock = artifacts.require('ERC20Mock') module.exports = function(deployer, network, accounts) { return deployer.then(async () => { - const { MERKLE_TREE_HEIGHT, AMOUNT, EMPTY_ELEMENT, ERC20_TOKEN } = process.env + const { MERKLE_TREE_HEIGHT, ETH_AMOUNT, EMPTY_ELEMENT, ERC20_TOKEN, TOKEN_AMOUNT } = process.env const verifier = await Verifier.deployed() const miMC = await MiMC.deployed() await ERC20Mixer.link(MiMC, miMC.address) @@ -20,11 +20,12 @@ module.exports = function(deployer, network, accounts) { const mixer = await deployer.deploy( ERC20Mixer, verifier.address, - AMOUNT, + ETH_AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, accounts[0], - token + token, + TOKEN_AMOUNT ) console.log('ERC20Mixer\'s address ', mixer.address) }) diff --git a/package.json b/package.json index 49b8f38..7ba1ccd 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "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" + "flat": "truffle-flattener contracts/ETHMixer.sol > ETHMixer_flat.sol contracts/ERC20Mixer.sol > ERC20Mixer_flat.sol" }, "keywords": [], "author": "", diff --git a/test/ERC20Mixer.test.js b/test/ERC20Mixer.test.js index 3a561d5..6d9aab2 100644 --- a/test/ERC20Mixer.test.js +++ b/test/ERC20Mixer.test.js @@ -5,17 +5,16 @@ require('chai') .should() const fs = require('fs') -const { toBN, toHex, randomHex } = require('web3-utils') +const { toBN, toHex } = require('web3-utils') const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper') const Mixer = artifacts.require('./ERC20Mixer.sol') const Token = artifacts.require('./ERC20Mock.sol') -const { AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env +const { ETH_AMOUNT, TOKEN_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') @@ -35,15 +34,6 @@ function generateDeposit() { 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) { @@ -52,24 +42,19 @@ function getRandomReceiver() { 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 => { +contract('ERC20Mixer', accounts => { let mixer let token 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 + const tokenDenomination = TOKEN_AMOUNT || '1000000000000000000' // 1 ether + const value = ETH_AMOUNT || '1000000000000000000' // 1 ether let snapshotId let prefix = 'test' let tree - const fee = bigInt(AMOUNT).shr(1) || bigInt(1e17) + const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17) const receiver = getRandomReceiver() const relayer = accounts[1] let groth16 @@ -85,7 +70,7 @@ contract('Mixer', accounts => { ) mixer = await Mixer.deployed() token = await Token.deployed() - await token.mint(sender, value) + await token.mint(sender, tokenDenomination) snapshotId = await takeSnapshot() groth16 = await buildGroth16() circuit = require('../build/circuits/withdraw.json') @@ -102,9 +87,9 @@ contract('Mixer', accounts => { describe('#deposit', () => { it('should work', async () => { const commitment = 43 - await token.approve(mixer.address, value) + await token.approve(mixer.address, tokenDenomination) - let { logs } = await mixer.deposit(commitment, { from: sender }) + 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)) @@ -117,17 +102,17 @@ contract('Mixer', accounts => { const deposit = generateDeposit() const user = accounts[4] await tree.insert(deposit.commitment) - await token.mint(user, value) + await token.mint(user, tokenDenomination) const balanceUserBefore = await token.balanceOf(user) - await token.approve(mixer.address, value, { from: user }) + await token.approve(mixer.address, tokenDenomination, { from: 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()), { from: user, gasPrice: '0' }) + await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' }) const balanceUserAfter = await token.balanceOf(user) - balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(value))) + balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination))) const { root, path_elements, path_index } = await tree.path(0) @@ -152,8 +137,9 @@ contract('Mixer', accounts => { const balanceMixerBefore = await token.balanceOf(mixer.address) const balanceRelayerBefore = await token.balanceOf(relayer) - const balanceOperatorBefore = await token.balanceOf(operator) + const ethBalanceOperatorBefore = await web3.eth.getBalance(operator) const balanceRecieverBefore = await token.balanceOf(toHex(receiver.toString())) + const ethBalanceRecieverBefore = await web3.eth.getBalance(toHex(receiver.toString())) let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) isSpent.should.be.equal(false) @@ -164,13 +150,15 @@ contract('Mixer', accounts => { const balanceMixerAfter = await token.balanceOf(mixer.address) const balanceRelayerAfter = await token.balanceOf(relayer) - const balanceOperatorAfter = await token.balanceOf(operator) + const ethBalanceOperatorAfter = await web3.eth.getBalance(operator) const balanceRecieverAfter = await token.balanceOf(toHex(receiver.toString())) + const ethBalanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString())) const feeBN = toBN(fee.toString()) - balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(value))) + balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination))) 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)) + ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore).add(feeBN)) + balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(tokenDenomination))) + ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).add(toBN(value)).sub(feeBN)) logs[0].event.should.be.equal('Withdraw') diff --git a/test/ETHMixer.test.js b/test/ETHMixer.test.js index 726abe1..14501c9 100644 --- a/test/ETHMixer.test.js +++ b/test/ETHMixer.test.js @@ -9,7 +9,7 @@ const { toBN, toHex, randomHex } = require('web3-utils') const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper') const Mixer = artifacts.require('./ETHMixer.sol') -const { AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env +const { ETH_AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env const websnarkUtils = require('websnark/src/utils') const buildGroth16 = require('websnark/src/groth16') @@ -57,17 +57,17 @@ function snarkVerify(proof) { return snarkjs['groth'].isValid(verification_key, proof, proof.publicSignals) } -contract('Mixer', accounts => { +contract('ETHMixer', 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 + const value = ETH_AMOUNT || '1000000000000000000' // 1 ether let snapshotId let prefix = 'test' let tree - const fee = bigInt(AMOUNT).shr(1) || bigInt(1e17) + const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17) const receiver = getRandomReceiver() const relayer = accounts[1] let groth16 @@ -90,8 +90,8 @@ contract('Mixer', accounts => { describe('#constructor', () => { it('should initialize', async () => { - const transferValue = await mixer.transferValue() - transferValue.should.be.eq.BN(toBN(value)) + const etherDenomination = await mixer.etherDenomination() + etherDenomination.should.be.eq.BN(toBN(value)) }) }) diff --git a/test/MerkleTreeWithHistory.test.js b/test/MerkleTreeWithHistory.test.js index 5d0b2d8..f6f5396 100644 --- a/test/MerkleTreeWithHistory.test.js +++ b/test/MerkleTreeWithHistory.test.js @@ -12,7 +12,7 @@ const MiMC = artifacts.require('./MiMC.sol') const MerkleTree = require('../lib/MerkleTree') const MimcHasher = require('../lib/MiMC') -const { AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env +const { ETH_AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env // eslint-disable-next-line no-unused-vars function BNArrayToStringArray(array) { @@ -30,7 +30,7 @@ contract('MerkleTreeWithHistory', accounts => { 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 diff --git a/truffle-config.js b/truffle-config.js index a9078e8..2e6b173 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -77,7 +77,7 @@ module.exports = { // Configure your compilers compilers: { solc: { - version: '0.5.10', // Fetch exact version from solc-bin (default: truffle's version) + version: '0.5.11', // Fetch exact version from solc-bin (default: truffle's version) // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) settings: { // See the solidity docs for advice about optimization and evmVersion optimizer: { From 5006006a20196815bc8049fd60e8c7f27133c0a8 Mon Sep 17 00:00:00 2001 From: Alexey Date: Wed, 28 Aug 2019 11:15:49 +0300 Subject: [PATCH 04/18] eslint vuln fix --- package-lock.json | 118 ++++++++++++++++++++++++++++++++++++++-------- package.json | 2 +- 2 files changed, 99 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa2d8c9..bd46e26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2362,9 +2362,9 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.0.1.tgz", - "integrity": "sha512-DyQRaMmORQ+JsWShYsSg4OPTjY56u1nCjAmICrE8vLWqyLKxhFXOthwMj1SA8xwfrv0CofLNVnqbfyhwCkaO0w==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.2.2.tgz", + "integrity": "sha512-mf0elOkxHbdyGX1IJEUsNBzCDdyoUgljF3rRlgfyYh0pwGnreLc0jjD6ZuleOibjmnUWZLY2eXwSooeOgGJ2jw==", "requires": { "@babel/code-frame": "^7.0.0", "ajv": "^6.10.0", @@ -2372,36 +2372,92 @@ "cross-spawn": "^6.0.5", "debug": "^4.0.1", "doctrine": "^3.0.0", - "eslint-scope": "^4.0.3", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^6.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.2", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.1", "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^3.1.0", + "glob-parent": "^5.0.0", "globals": "^11.7.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.2.2", + "inquirer": "^6.4.1", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.3.0", - "lodash": "^4.17.11", + "lodash": "^4.17.14", "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", "optionator": "^0.8.2", "progress": "^2.0.0", "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", "table": "^5.2.3", - "text-table": "^0.2.0" + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", + "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", + "requires": { + "eslint-visitor-keys": "^1.0.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==" + }, + "glob-parent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", + "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==" + } } }, "eslint-scope": { @@ -2427,13 +2483,30 @@ "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==" }, "espree": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.0.0.tgz", - "integrity": "sha512-lJvCS6YbCn3ImT3yKkPe0+tJ+mH6ljhGNjHQH9mRtiO6gjhVAOhVXW1yjnwqGwTkK3bGbye+hb00nFNmu0l/1Q==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.1.tgz", + "integrity": "sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ==", "requires": { - "acorn": "^6.0.7", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" + "acorn": "^7.0.0", + "acorn-jsx": "^5.0.2", + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "acorn": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.0.0.tgz", + "integrity": "sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ==" + }, + "acorn-jsx": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz", + "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==" + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==" + } } }, "esprima": { @@ -9872,6 +9945,11 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", "integrity": "sha1-wqMN7bPlNdcsz4LjQ5QaULqFM6w=" }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==" + }, "v8flags": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", diff --git a/package.json b/package.json index 7ba1ccd..34daa2e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "circom": "0.0.30", "circomlib": "^0.0.10", "dotenv": "^8.0.0", - "eslint": "^6.0.1", + "eslint": "^6.2.2", "ganache-cli": "^6.4.5", "snarkjs": "^0.1.16", "truffle": "^5.0.27", From b8c0c1898f56199fcbeb8172ced16ac01337678b Mon Sep 17 00:00:00 2001 From: Alexey Date: Fri, 30 Aug 2019 13:06:17 +0300 Subject: [PATCH 05/18] test with real DAI and USDT --- .env.example | 9 ++ contracts/ERC20Mixer.sol | 47 +++++++- contracts/Mocks/IUSDT.sol | 18 +++ migrations/5_deploy_erc20_mixer.js | 2 +- test/ERC20Mixer.test.js | 176 ++++++++++++++++++++++++++++- 5 files changed, 242 insertions(+), 10 deletions(-) create mode 100644 contracts/Mocks/IUSDT.sol diff --git a/.env.example b/.env.example index c5c296e..55b74ea 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,12 @@ TOKEN_AMOUNT=100000000000000000 EMPTY_ELEMENT=1337 PRIVATE_KEY= ERC20_TOKEN= + +# DAI mirror in Kovan +#ERC20_TOKEN=0xd2b1a6b34f4a68425e7c28b4db5a37be3b7a4947 +# block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some DAI is 13146218 + +# USDT mirror in Kovan +#ERC20_TOKEN=0xf3e0d7bf58c5d455d31ef1c2d5375904df525105 +#TOKEN_AMOUNT=1000000 +# block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some USDT is 13147586 diff --git a/contracts/ERC20Mixer.sol b/contracts/ERC20Mixer.sol index aedbcea..d16a50b 100644 --- a/contracts/ERC20Mixer.sol +++ b/contracts/ERC20Mixer.sol @@ -12,10 +12,9 @@ pragma solidity ^0.5.8; import "./Mixer.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract ERC20Mixer is Mixer { - IERC20 public token; + address public token; // mixed token amount uint256 public tokenDenomination; // ether value to cover network fee (for relayer) and to have some ETH on a brand new address @@ -27,7 +26,7 @@ contract ERC20Mixer is Mixer { uint8 _merkleTreeHeight, uint256 _emptyElement, address payable _operator, - IERC20 _token, + address _token, uint256 _tokenDenomination ) Mixer(_verifier, _merkleTreeHeight, _emptyElement, _operator) public { token = _token; @@ -42,7 +41,7 @@ contract ERC20Mixer is Mixer { */ function deposit(uint256 commitment) public payable { require(msg.value == etherFeeDenomination, "Please send `etherFeeDenomination` ETH along with transaction"); - require(token.transferFrom(msg.sender, address(this), tokenDenomination), "Approve before using"); + transferFrom(msg.sender, address(this), tokenDenomination); _deposit(commitment); emit Deposit(commitment, next_index - 1, block.timestamp); @@ -69,8 +68,46 @@ contract ERC20Mixer is Mixer { operator.transfer(fee); } - token.transfer(receiver, tokenDenomination); + transfer(receiver, tokenDenomination); emit Withdraw(receiver, nullifierHash, fee); } + + function transferFrom(address from, address to, uint256 amount) internal { + bool success; + bytes memory data; + bytes4 transferFromSelector = 0x23b872dd; + (success, data) = token.call( + abi.encodeWithSelector( + transferFromSelector, + from, to, amount + ) + ); + require(success, "not enough allowed tokens"); + if (data.length > 0) { + assembly { + success := mload(add(data, 0x20)) + } + require(success, "not enough allowed tokens"); + } + } + + function transfer(address to, uint256 amount) internal { + bool success; + bytes memory data; + bytes4 transferSelector = 0xa9059cbb; + (success, data) = token.call( + abi.encodeWithSelector( + transferSelector, + to, amount + ) + ); + require(success, "not enough tokens"); + if (data.length > 0) { + assembly { + success := mload(add(data, 0x20)) + } + require(success, "not enough tokens"); + } + } } diff --git a/contracts/Mocks/IUSDT.sol b/contracts/Mocks/IUSDT.sol new file mode 100644 index 0000000..8bd9ead --- /dev/null +++ b/contracts/Mocks/IUSDT.sol @@ -0,0 +1,18 @@ +contract ERC20Basic { + uint public _totalSupply; + function totalSupply() public view returns (uint); + function balanceOf(address who) public view returns (uint); + function transfer(address to, uint value) public; + event Transfer(address indexed from, address indexed to, uint value); +} + +/** + * @title ERC20 interface + * @dev see https://github.com/ethereum/EIPs/issues/20 + */ +contract IUSDT is ERC20Basic { + function allowance(address owner, address spender) public view returns (uint); + function transferFrom(address from, address to, uint value) public; + function approve(address spender, uint value) public; + event Approval(address indexed owner, address indexed spender, uint value); +} diff --git a/migrations/5_deploy_erc20_mixer.js b/migrations/5_deploy_erc20_mixer.js index b710d9e..8ee907c 100644 --- a/migrations/5_deploy_erc20_mixer.js +++ b/migrations/5_deploy_erc20_mixer.js @@ -13,7 +13,7 @@ module.exports = function(deployer, network, accounts) { const miMC = await MiMC.deployed() await ERC20Mixer.link(MiMC, miMC.address) let token = ERC20_TOKEN - if(deployer.network !== 'mainnet') { + if(token === '') { const tokenInstance = await deployer.deploy(ERC20Mock) token = tokenInstance.address } diff --git a/test/ERC20Mixer.test.js b/test/ERC20Mixer.test.js index 6d9aab2..a0432cd 100644 --- a/test/ERC20Mixer.test.js +++ b/test/ERC20Mixer.test.js @@ -10,7 +10,8 @@ const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper') const Mixer = artifacts.require('./ERC20Mixer.sol') const Token = artifacts.require('./ERC20Mock.sol') -const { ETH_AMOUNT, TOKEN_AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env +const USDTToken = artifacts.require('./IUSDT.sol') +const { ETH_AMOUNT, TOKEN_AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, ERC20_TOKEN } = process.env const websnarkUtils = require('websnark/src/utils') const buildGroth16 = require('websnark/src/groth16') @@ -45,11 +46,12 @@ function getRandomReceiver() { contract('ERC20Mixer', accounts => { let mixer let token + let usdtToken const sender = accounts[0] const operator = accounts[0] const levels = MERKLE_TREE_HEIGHT || 16 const zeroValue = EMPTY_ELEMENT || 1337 - const tokenDenomination = TOKEN_AMOUNT || '1000000000000000000' // 1 ether + let tokenDenomination = TOKEN_AMOUNT || '1000000000000000000' // 1 ether const value = ETH_AMOUNT || '1000000000000000000' // 1 ether let snapshotId let prefix = 'test' @@ -69,8 +71,13 @@ contract('ERC20Mixer', accounts => { prefix, ) mixer = await Mixer.deployed() - token = await Token.deployed() - await token.mint(sender, tokenDenomination) + 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) + } snapshotId = await takeSnapshot() groth16 = await buildGroth16() circuit = require('../build/circuits/withdraw.json') @@ -161,6 +168,167 @@ contract('ERC20Mixer', accounts => { ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).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.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()) + await 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(mixer.address, tokenDenomination, { from: user }) + console.log('approve done') + const allowanceUser = await usdtToken.allowance(user, mixer.address) + console.log('allowanceUser', allowanceUser.toString()) + await mixer.deposit(toBN(deposit.commitment.toString()), { value, 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 { 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 usdtToken.balanceOf(mixer.address) + const balanceRelayerBefore = await usdtToken.balanceOf(relayer) + const ethBalanceOperatorBefore = await web3.eth.getBalance(operator) + const balanceRecieverBefore = await usdtToken.balanceOf(toHex(receiver.toString())) + const ethBalanceRecieverBefore = 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 usdtToken.balanceOf(mixer.address) + const balanceRelayerAfter = await usdtToken.balanceOf(relayer) + const ethBalanceOperatorAfter = await web3.eth.getBalance(operator) + const balanceRecieverAfter = await usdtToken.balanceOf(toHex(receiver.toString())) + const ethBalanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString())) + const feeBN = toBN(fee.toString()) + balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination))) + balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore)) + ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore).add(feeBN)) + balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(tokenDenomination))) + ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).add(toBN(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.skip('should work with REAL DAI', async () => { + // dont forget to specify your token in .env + // and sent `tokenDenomination` to accounts[0] (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1) + // run ganache as + // 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()) + await 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(mixer.address, tokenDenomination, { from: user }) + console.log('approve done') + await mixer.deposit(toBN(deposit.commitment.toString()), { value, 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 { 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 token.balanceOf(mixer.address) + const balanceRelayerBefore = await token.balanceOf(relayer) + const ethBalanceOperatorBefore = await web3.eth.getBalance(operator) + const balanceRecieverBefore = await token.balanceOf(toHex(receiver.toString())) + const ethBalanceRecieverBefore = 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' }) + console.log('withdraw done') + + const balanceMixerAfter = await token.balanceOf(mixer.address) + const balanceRelayerAfter = await token.balanceOf(relayer) + const ethBalanceOperatorAfter = await web3.eth.getBalance(operator) + const balanceRecieverAfter = await token.balanceOf(toHex(receiver.toString())) + const ethBalanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString())) + const feeBN = toBN(fee.toString()) + balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination))) + balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore)) + ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore).add(feeBN)) + balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(tokenDenomination))) + ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).add(toBN(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) From e0ec5757451b571a6aefee99fc102cbd4ee58594 Mon Sep 17 00:00:00 2001 From: Alexey Date: Fri, 30 Aug 2019 13:21:53 +0300 Subject: [PATCH 06/18] gas tuning --- .env.example | 4 ++-- truffle-config.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 55b74ea..6e36a35 100644 --- a/.env.example +++ b/.env.example @@ -8,9 +8,9 @@ ERC20_TOKEN= # DAI mirror in Kovan #ERC20_TOKEN=0xd2b1a6b34f4a68425e7c28b4db5a37be3b7a4947 -# block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some DAI is 13146218 +# the block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some DAI is 13146218 # USDT mirror in Kovan #ERC20_TOKEN=0xf3e0d7bf58c5d455d31ef1c2d5375904df525105 #TOKEN_AMOUNT=1000000 -# block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some USDT is 13147586 +# the block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some USDT is 13147586 diff --git a/truffle-config.js b/truffle-config.js index 2e6b173..f510404 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -45,7 +45,7 @@ module.exports = { kovan: { provider: () => new HDWalletProvider(process.env.PRIVATE_KEY, 'https://kovan.infura.io/v3/c7463beadf2144e68646ff049917b716'), network_id: 42, - gas: 7000000, + gas: 6000000, gasPrice: utils.toWei('1', 'gwei'), // confirmations: 0, // timeoutBlocks: 200, From 0e0c87d5336b35e23d10720c8304fbaa24346c52 Mon Sep 17 00:00:00 2001 From: Alexey Date: Mon, 2 Sep 2019 17:56:10 +0300 Subject: [PATCH 07/18] CLI fix --- cli.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli.js b/cli.js index f5f604c..bc88ceb 100755 --- a/cli.js +++ b/cli.js @@ -99,7 +99,7 @@ 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() + contractJson = await (await fetch('build/contracts/ETHMixer.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 @@ -107,7 +107,7 @@ async function init() { EMPTY_ELEMENT = 0 } else { web3 = new Web3('http://localhost:8545', null, { transactionConfirmationBlocks: 1 }) - contractJson = require('./build/contracts/Mixer.json') + contractJson = require('./build/contracts/ETHMixer.json') circuit = require('./build/circuits/withdraw.json') proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer require('dotenv').config() From ec30e2d357c5995c6d942703a708e9b1849784fc Mon Sep 17 00:00:00 2001 From: Drygin Date: Wed, 4 Sep 2019 13:31:10 +0300 Subject: [PATCH 08/18] fix merkle tree capacity --- contracts/MerkleTreeWithHistory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/MerkleTreeWithHistory.sol b/contracts/MerkleTreeWithHistory.sol index a374bff..74a4bc3 100644 --- a/contracts/MerkleTreeWithHistory.sol +++ b/contracts/MerkleTreeWithHistory.sol @@ -58,7 +58,7 @@ contract MerkleTreeWithHistory { function _insert(uint256 leaf) internal { uint32 current_index = next_index; - require(current_index != 2**(levels - 1), "Merkle tree is full"); + require(current_index != 2**levels, "Merkle tree is full"); next_index += 1; uint256 current_level_hash = leaf; uint256 left; From 374dd420f527ea8e104f42a610dc1cc5cae508c4 Mon Sep 17 00:00:00 2001 From: Drygin Date: Wed, 4 Sep 2019 16:08:11 +0300 Subject: [PATCH 09/18] fix test of capacity of Merkle tree --- test/MerkleTreeWithHistory.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/MerkleTreeWithHistory.test.js b/test/MerkleTreeWithHistory.test.js index 5d0b2d8..2bcd505 100644 --- a/test/MerkleTreeWithHistory.test.js +++ b/test/MerkleTreeWithHistory.test.js @@ -180,7 +180,7 @@ contract('MerkleTreeWithHistory', accounts => { zeroValue = 1337 merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, zeroValue) - for (let i = 0; i < 2**(levels - 1); i++) { + for (let i = 0; i < 2**levels; i++) { await merkleTreeWithHistory.insert(i+42).should.be.fulfilled } From 50872ac342de791e32c4892c4d5a8bdea3651e6e Mon Sep 17 00:00:00 2001 From: Roman Storm Date: Fri, 6 Sep 2019 17:22:30 -0400 Subject: [PATCH 10/18] change fee structure --- contracts/ERC20Mixer.sol | 58 +++++++++++----------------------------- contracts/ETHMixer.sol | 44 +++++++----------------------- contracts/Mixer.sol | 40 ++++++++++++++++++++++----- test/ERC20Mixer.test.js | 15 +++++------ test/ETHMixer.test.js | 2 +- 5 files changed, 65 insertions(+), 94 deletions(-) diff --git a/contracts/ERC20Mixer.sol b/contracts/ERC20Mixer.sol index d16a50b..b36a581 100644 --- a/contracts/ERC20Mixer.sol +++ b/contracts/ERC20Mixer.sol @@ -15,65 +15,37 @@ import "./Mixer.sol"; contract ERC20Mixer is Mixer { address public token; - // mixed token amount - uint256 public tokenDenomination; // ether value to cover network fee (for relayer) and to have some ETH on a brand new address - uint256 public etherFeeDenomination; + uint256 public userEther; constructor( address _verifier, - uint256 _etherFeeDenomination, + uint256 _userEther, uint8 _merkleTreeHeight, uint256 _emptyElement, address payable _operator, address _token, - uint256 _tokenDenomination - ) Mixer(_verifier, _merkleTreeHeight, _emptyElement, _operator) public { + uint256 _mixDenomination + ) Mixer(_verifier, _mixDenomination, _merkleTreeHeight, _emptyElement, _operator) public { token = _token; - tokenDenomination = _tokenDenomination; - etherFeeDenomination = _etherFeeDenomination; + userEther = _userEther; } - /** - @dev Deposit funds into the mixer. The caller must send ETH value equal to `etherFeeDenomination` of this mixer. - The caller also has to have at least `tokenDenomination` amount approved for the mixer. - @param commitment the note commitment, which is PedersenHash(nullifier + secret) - */ - function deposit(uint256 commitment) public payable { - require(msg.value == etherFeeDenomination, "Please send `etherFeeDenomination` ETH along with transaction"); - transferFrom(msg.sender, address(this), tokenDenomination); - _deposit(commitment); - - emit Deposit(commitment, next_index - 1, block.timestamp); + function _processDeposit() internal { + require(msg.value == userEther, "Please send `userEther` ETH along with transaction"); + safeErc20TransferFrom(msg.sender, address(this), mixDenomination); } - /** - @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 { - _withdraw(a, b, c, input); - address payable receiver = address(input[2]); - uint256 fee = input[3]; - uint256 nullifierHash = input[1]; + function _processWithdraw(address payable _receiver, uint256 _fee) internal { + _receiver.transfer(userEther); - require(fee < etherFeeDenomination, "Fee exceeds transfer value"); - receiver.transfer(etherFeeDenomination - fee); - - if (fee > 0) { - operator.transfer(fee); + safeErc20Transfer(_receiver, mixDenomination - _fee); + if (_fee > 0) { + safeErc20Transfer(operator, _fee); } - - transfer(receiver, tokenDenomination); - - emit Withdraw(receiver, nullifierHash, fee); } - function transferFrom(address from, address to, uint256 amount) internal { + function safeErc20TransferFrom(address from, address to, uint256 amount) internal { bool success; bytes memory data; bytes4 transferFromSelector = 0x23b872dd; @@ -92,7 +64,7 @@ contract ERC20Mixer is Mixer { } } - function transfer(address to, uint256 amount) internal { + function safeErc20Transfer(address to, uint256 amount) internal { bool success; bytes memory data; bytes4 transferSelector = 0xa9059cbb; diff --git a/contracts/ETHMixer.sol b/contracts/ETHMixer.sol index f194740..5da14b9 100644 --- a/contracts/ETHMixer.sol +++ b/contracts/ETHMixer.sol @@ -14,49 +14,23 @@ pragma solidity ^0.5.8; import "./Mixer.sol"; contract ETHMixer is Mixer { - uint256 public etherDenomination; - constructor( address _verifier, - uint256 _etherDenomination, + uint256 _mixDenomination, uint8 _merkleTreeHeight, uint256 _emptyElement, address payable _operator - ) Mixer(_verifier, _merkleTreeHeight, _emptyElement, _operator) public { - etherDenomination = _etherDenomination; + ) Mixer(_verifier, _mixDenomination, _merkleTreeHeight, _emptyElement, _operator) public { } - /** - @dev Deposit funds into mixer. The caller must send value equal to `etherDenomination` of this mixer. - @param commitment the note commitment, which is PedersenHash(nullifier + secret) - */ - function deposit(uint256 commitment) public payable { - require(msg.value == etherDenomination, "Please send `etherDenomination` ETH along with transaction"); - _deposit(commitment); - - 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 { - _withdraw(a, b, c, input); - address payable receiver = address(input[2]); - uint256 fee = input[3]; - uint256 nullifierHash = input[1]; - - require(fee < etherDenomination, "Fee exceeds transfer value"); - receiver.transfer(etherDenomination - fee); - if (fee > 0) { - operator.transfer(fee); + function _processWithdraw(address payable _receiver, uint256 _fee) internal { + _receiver.transfer(mixDenomination - _fee); + if (_fee > 0) { + operator.transfer(_fee); } + } - emit Withdraw(receiver, nullifierHash, fee); + function _processDeposit() internal { + require(msg.value == mixDenomination, "Please send `mixDenomination` ETH along with transaction"); } } diff --git a/contracts/Mixer.sol b/contracts/Mixer.sol index 653167a..2014d84 100644 --- a/contracts/Mixer.sol +++ b/contracts/Mixer.sol @@ -26,6 +26,7 @@ contract Mixer is MerkleTreeWithHistory { // we store all commitments just to prevent accidental deposits with the same commitment mapping(uint256 => bool) public commitments; IVerifier public verifier; + uint256 public mixDenomination; event Deposit(uint256 indexed commitment, uint256 leafIndex, uint256 timestamp); event Withdraw(address to, uint256 nullifierHash, uint256 fee); @@ -39,31 +40,54 @@ contract Mixer is MerkleTreeWithHistory { */ constructor( address _verifier, + uint256 _mixDenomination, uint8 _merkleTreeHeight, uint256 _emptyElement, address payable _operator ) MerkleTreeWithHistory(_merkleTreeHeight, _emptyElement) public { verifier = IVerifier(_verifier); operator = _operator; + mixDenomination = _mixDenomination; } - - function _deposit(uint256 commitment) internal { + /** + @dev Deposit funds into mixer. The caller must send value equal to `mixDenomination` of this mixer. + @param commitment the note commitment, which is PedersenHash(nullifier + secret) + */ + /** + @dev Deposit funds into the mixer. The caller must send ETH value equal to `userEther` of this mixer. + The caller also has to have at least `mixDenomination` amount approved for the mixer. + @param commitment the note commitment, which is PedersenHash(nullifier + secret) + */ + function deposit(uint256 commitment) public payable { require(isDepositsEnabled, "deposits disabled"); require(!commitments[commitment], "The commitment has been submitted"); + _processDeposit(); _insert(commitment); commitments[commitment] = true; - } - function _withdraw(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[4] memory input) internal { + 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(fee < mixDenomination, "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(a, b, c, input), "Invalid withdraw proof"); - nullifierHashes[nullifierHash] = true; + _processWithdraw(receiver, fee); + emit Withdraw(receiver, nullifierHash, fee); } function toggleDeposits() external { @@ -79,4 +103,8 @@ contract Mixer is MerkleTreeWithHistory { function isSpent(uint256 nullifier) public view returns(bool) { return nullifierHashes[nullifier]; } + + function _processDeposit() internal {} + function _processWithdraw(address payable _receiver, uint256 _fee) internal {} + } diff --git a/test/ERC20Mixer.test.js b/test/ERC20Mixer.test.js index a0432cd..4a8fa79 100644 --- a/test/ERC20Mixer.test.js +++ b/test/ERC20Mixer.test.js @@ -122,7 +122,6 @@ contract('ERC20Mixer', accounts => { balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination))) const { root, path_elements, path_index } = await tree.path(0) - // Circuit input const input = stringifyBigInts({ // public @@ -149,7 +148,6 @@ contract('ERC20Mixer', accounts => { const ethBalanceRecieverBefore = 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) @@ -161,12 +159,11 @@ contract('ERC20Mixer', accounts => { const balanceRecieverAfter = await token.balanceOf(toHex(receiver.toString())) const ethBalanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString())) const feeBN = toBN(fee.toString()) - balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination))) + balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(value))) balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore)) - ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore).add(feeBN)) - balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(tokenDenomination))) - ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).add(toBN(value)).sub(feeBN)) - + ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore)) + balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(tokenDenomination).sub(feeBN))) + ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).add(toBN(value))) logs[0].event.should.be.equal('Withdraw') logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString())) @@ -258,9 +255,9 @@ contract('ERC20Mixer', accounts => { }) it.skip('should work with REAL DAI', async () => { // dont forget to specify your token in .env - // and sent `tokenDenomination` to accounts[0] (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1) + // and send `tokenDenomination` to accounts[0] (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1) // run ganache as - // ganache-cli --fork https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448@13146218 -d --keepAliveTimeout 20 + // 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) diff --git a/test/ETHMixer.test.js b/test/ETHMixer.test.js index 14501c9..32a3721 100644 --- a/test/ETHMixer.test.js +++ b/test/ETHMixer.test.js @@ -90,7 +90,7 @@ contract('ETHMixer', accounts => { describe('#constructor', () => { it('should initialize', async () => { - const etherDenomination = await mixer.etherDenomination() + const etherDenomination = await mixer.mixDenomination() etherDenomination.should.be.eq.BN(toBN(value)) }) }) From 9009b9c56d48ac82f2724e44dcb97fc29f334713 Mon Sep 17 00:00:00 2001 From: poma Date: Fri, 6 Sep 2019 16:54:37 -0400 Subject: [PATCH 11/18] custom relayer --- circuits/withdraw.circom | 3 +++ cli.js | 1 + contracts/ERC20Mixer.sol | 4 ++-- contracts/ETHMixer.sol | 4 ++-- contracts/Mixer.sol | 15 ++++++++------- test/ERC20Mixer.test.js | 6 ++++++ test/ETHMixer.test.js | 8 ++++++++ 7 files changed, 30 insertions(+), 11 deletions(-) diff --git a/circuits/withdraw.circom b/circuits/withdraw.circom index b94fe19..27612d8 100644 --- a/circuits/withdraw.circom +++ b/circuits/withdraw.circom @@ -31,6 +31,7 @@ template Withdraw(levels, rounds) { signal input root; signal input nullifierHash; signal input receiver; // 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 private input nullifier; signal private input secret; @@ -56,8 +57,10 @@ template Withdraw(levels, rounds) { // Squares are used to prevent optimizer from removing those constraints signal receiverSquare; signal feeSquare; + signal relayerSquare; receiverSquare <== receiver * receiver; feeSquare <== fee * fee; + relayerSquare <== relayer * relayer; } component main = Withdraw(16, 220); diff --git a/cli.js b/cli.js index bc88ceb..34d3f05 100755 --- a/cli.js +++ b/cli.js @@ -75,6 +75,7 @@ async function withdraw(note, receiver) { root: root, nullifierHash, receiver: bigInt(receiver), + relayer: bigInt(0), fee: bigInt(0), // private diff --git a/contracts/ERC20Mixer.sol b/contracts/ERC20Mixer.sol index b36a581..aeb1ac9 100644 --- a/contracts/ERC20Mixer.sol +++ b/contracts/ERC20Mixer.sol @@ -36,12 +36,12 @@ contract ERC20Mixer is Mixer { safeErc20TransferFrom(msg.sender, address(this), mixDenomination); } - function _processWithdraw(address payable _receiver, uint256 _fee) internal { + function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee) internal { _receiver.transfer(userEther); safeErc20Transfer(_receiver, mixDenomination - _fee); if (_fee > 0) { - safeErc20Transfer(operator, _fee); + safeErc20Transfer(_relayer, _fee); } } diff --git a/contracts/ETHMixer.sol b/contracts/ETHMixer.sol index 5da14b9..107fe6c 100644 --- a/contracts/ETHMixer.sol +++ b/contracts/ETHMixer.sol @@ -23,10 +23,10 @@ contract ETHMixer is Mixer { ) Mixer(_verifier, _mixDenomination, _merkleTreeHeight, _emptyElement, _operator) public { } - function _processWithdraw(address payable _receiver, uint256 _fee) internal { + function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee) internal { _receiver.transfer(mixDenomination - _fee); if (_fee > 0) { - operator.transfer(_fee); + _relayer.transfer(_fee); } } diff --git a/contracts/Mixer.sol b/contracts/Mixer.sol index 2014d84..7cae6f2 100644 --- a/contracts/Mixer.sol +++ b/contracts/Mixer.sol @@ -14,7 +14,7 @@ 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); + function verifyProof(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[5] memory input) public returns(bool); } contract Mixer is MerkleTreeWithHistory { @@ -29,7 +29,7 @@ contract Mixer is MerkleTreeWithHistory { uint256 public mixDenomination; event Deposit(uint256 indexed commitment, uint256 leafIndex, uint256 timestamp); - event Withdraw(address to, uint256 nullifierHash, uint256 fee); + event Withdraw(address to, uint256 nullifierHash, address relayer, uint256 fee); /** @dev The constructor @@ -75,19 +75,20 @@ contract Mixer is MerkleTreeWithHistory { - 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 { + function withdraw(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[5] memory input) public { uint256 root = input[0]; uint256 nullifierHash = input[1]; address payable receiver = address(input[2]); - uint256 fee = input[3]; + address payable relayer = address(input[3]); + uint256 fee = input[4]; require(fee < mixDenomination, "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(a, b, c, input), "Invalid withdraw proof"); nullifierHashes[nullifierHash] = true; - _processWithdraw(receiver, fee); - emit Withdraw(receiver, nullifierHash, fee); + _processWithdraw(receiver, relayer, fee); + emit Withdraw(receiver, nullifierHash, relayer, fee); } function toggleDeposits() external { @@ -105,6 +106,6 @@ contract Mixer is MerkleTreeWithHistory { } function _processDeposit() internal {} - function _processWithdraw(address payable _receiver, uint256 _fee) internal {} + function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee) internal {} } diff --git a/test/ERC20Mixer.test.js b/test/ERC20Mixer.test.js index 4a8fa79..e9f72c2 100644 --- a/test/ERC20Mixer.test.js +++ b/test/ERC20Mixer.test.js @@ -127,6 +127,7 @@ contract('ERC20Mixer', accounts => { // public root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), + relayer: operator, receiver, fee, @@ -167,6 +168,7 @@ contract('ERC20Mixer', accounts => { logs[0].event.should.be.equal('Withdraw') 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 mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) isSpent.should.be.equal(true) @@ -207,6 +209,7 @@ contract('ERC20Mixer', accounts => { // public root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), + relayer: operator, receiver, fee, @@ -249,6 +252,7 @@ contract('ERC20Mixer', accounts => { logs[0].event.should.be.equal('Withdraw') 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 mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) isSpent.should.be.equal(true) @@ -285,6 +289,7 @@ contract('ERC20Mixer', accounts => { // public root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), + relayer: operator, receiver, fee, @@ -328,6 +333,7 @@ contract('ERC20Mixer', accounts => { logs[0].event.should.be.equal('Withdraw') 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 mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) isSpent.should.be.equal(true) diff --git a/test/ETHMixer.test.js b/test/ETHMixer.test.js index 32a3721..cd6753b 100644 --- a/test/ETHMixer.test.js +++ b/test/ETHMixer.test.js @@ -141,6 +141,7 @@ contract('ETHMixer', accounts => { root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifier: deposit.nullifier, + relayer: operator, receiver, fee, secret: deposit.secret, @@ -196,6 +197,7 @@ contract('ETHMixer', accounts => { // public root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), + relayer: operator, receiver, fee, @@ -235,6 +237,7 @@ contract('ETHMixer', accounts => { logs[0].event.should.be.equal('Withdraw') 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 mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) isSpent.should.be.equal(true) @@ -251,6 +254,7 @@ contract('ETHMixer', accounts => { root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifier: deposit.nullifier, + relayer: operator, receiver, fee, secret: deposit.secret, @@ -275,6 +279,7 @@ contract('ETHMixer', accounts => { root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifier: deposit.nullifier, + relayer: operator, receiver, fee, secret: deposit.secret, @@ -299,6 +304,7 @@ contract('ETHMixer', accounts => { root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifier: deposit.nullifier, + relayer: operator, receiver, fee: oneEtherFee, secret: deposit.secret, @@ -323,6 +329,7 @@ contract('ETHMixer', accounts => { nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), root, nullifier: deposit.nullifier, + relayer: operator, receiver, fee, secret: deposit.secret, @@ -350,6 +357,7 @@ contract('ETHMixer', accounts => { root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifier: deposit.nullifier, + relayer: operator, receiver, fee, secret: deposit.secret, From 010837f92b4432a77f21164f182ae2127faf025a Mon Sep 17 00:00:00 2001 From: Alexey Date: Tue, 10 Sep 2019 16:31:34 +0300 Subject: [PATCH 12/18] fix tests --- contracts/ERC20Mixer.sol | 4 ++++ contracts/Mixer.sol | 2 +- test/ERC20Mixer.test.js | 8 ++++---- test/MerkleTreeWithHistory.test.js | 9 +++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/contracts/ERC20Mixer.sol b/contracts/ERC20Mixer.sol index aeb1ac9..8a815c5 100644 --- a/contracts/ERC20Mixer.sol +++ b/contracts/ERC20Mixer.sol @@ -56,6 +56,8 @@ contract ERC20Mixer is Mixer { ) ); require(success, "not enough allowed tokens"); + + // if contract returns some data let's make sure that is `true` according to standard if (data.length > 0) { assembly { success := mload(add(data, 0x20)) @@ -75,6 +77,8 @@ contract ERC20Mixer is Mixer { ) ); require(success, "not enough tokens"); + + // if contract returns some data let's make sure that is `true` according to standard if (data.length > 0) { assembly { success := mload(add(data, 0x20)) diff --git a/contracts/Mixer.sol b/contracts/Mixer.sol index 7cae6f2..262967c 100644 --- a/contracts/Mixer.sol +++ b/contracts/Mixer.sol @@ -29,7 +29,7 @@ contract Mixer is MerkleTreeWithHistory { uint256 public mixDenomination; event Deposit(uint256 indexed commitment, uint256 leafIndex, uint256 timestamp); - event Withdraw(address to, uint256 nullifierHash, address relayer, uint256 fee); + event Withdraw(address to, uint256 nullifierHash, address indexed relayer, uint256 fee); /** @dev The constructor diff --git a/test/ERC20Mixer.test.js b/test/ERC20Mixer.test.js index e9f72c2..8c69835 100644 --- a/test/ERC20Mixer.test.js +++ b/test/ERC20Mixer.test.js @@ -127,7 +127,7 @@ contract('ERC20Mixer', accounts => { // public root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), - relayer: operator, + relayer, receiver, fee, @@ -160,15 +160,15 @@ contract('ERC20Mixer', accounts => { const balanceRecieverAfter = await token.balanceOf(toHex(receiver.toString())) const ethBalanceRecieverAfter = 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)) + balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination))) + balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore).add(feeBN)) ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore)) balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(tokenDenomination).sub(feeBN))) ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).add(toBN(value))) logs[0].event.should.be.equal('Withdraw') logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString())) - logs[0].args.relayer.should.be.eq.BN(operator) + logs[0].args.relayer.should.be.eq.BN(relayer) 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) diff --git a/test/MerkleTreeWithHistory.test.js b/test/MerkleTreeWithHistory.test.js index f6f5396..742c00c 100644 --- a/test/MerkleTreeWithHistory.test.js +++ b/test/MerkleTreeWithHistory.test.js @@ -190,6 +190,15 @@ contract('MerkleTreeWithHistory', accounts => { error = await merkleTreeWithHistory.insert(1).should.be.rejected error.reason.should.be.equal('Merkle tree is full') }) + + it.skip('mimc gas', async () => { + levels = 6 + zeroValue = 1337 + merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, zeroValue) + + const gas = await merkleTreeWithHistory.hashLeftRight.estimateGas(zeroValue, zeroValue) + console.log('gas', gas - 21000) + }) }) afterEach(async () => { From 3f4e686899a940a717c6d2663262ff5ffa507bd8 Mon Sep 17 00:00:00 2001 From: Alexey Date: Tue, 10 Sep 2019 17:31:19 +0300 Subject: [PATCH 13/18] fix cli.js --- cli.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cli.js b/cli.js index d2a4e62..0a75947 100755 --- a/cli.js +++ b/cli.js @@ -118,8 +118,6 @@ async function getBalance(receiver) { console.log('Balance is ', web3.utils.fromWei(balance)) } -const inBrowser = (typeof window !== 'undefined') - /** * Init web3, contracts, and snark */ @@ -133,7 +131,7 @@ async function init() { 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 + ETH_AMOUNT = 1e18 EMPTY_ELEMENT = 1 } else { // Initialize from local node From 3404acef48a0d7b0d152db949b542c214f0cd221 Mon Sep 17 00:00:00 2001 From: Alexey Date: Wed, 11 Sep 2019 11:17:32 +0300 Subject: [PATCH 14/18] fix leaf count --- contracts/MerkleTreeWithHistory.sol | 2 +- test/MerkleTreeWithHistory.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/MerkleTreeWithHistory.sol b/contracts/MerkleTreeWithHistory.sol index 23c2532..23412a2 100644 --- a/contracts/MerkleTreeWithHistory.sol +++ b/contracts/MerkleTreeWithHistory.sol @@ -58,7 +58,7 @@ contract MerkleTreeWithHistory { function _insert(uint256 leaf) internal { uint32 current_index = next_index; - require(current_index != 2**levels, "Merkle tree is full"); + require(current_index != 2**(levels - 1), "Merkle tree is full"); next_index += 1; uint256 current_level_hash = leaf; uint256 left; diff --git a/test/MerkleTreeWithHistory.test.js b/test/MerkleTreeWithHistory.test.js index 730c25c..742c00c 100644 --- a/test/MerkleTreeWithHistory.test.js +++ b/test/MerkleTreeWithHistory.test.js @@ -180,7 +180,7 @@ contract('MerkleTreeWithHistory', accounts => { zeroValue = 1337 merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, zeroValue) - for (let i = 0; i < 2**levels; i++) { + for (let i = 0; i < 2**(levels - 1); i++) { await merkleTreeWithHistory.insert(i+42).should.be.fulfilled } From 0689d76df1c2192b58a84dbe53a96fc3fbdf1e8f Mon Sep 17 00:00:00 2001 From: Alexey Date: Wed, 11 Sep 2019 11:25:32 +0300 Subject: [PATCH 15/18] leafs = 2**(levels - 1) --- contracts/MerkleTreeWithHistory.sol | 2 +- test/MerkleTreeWithHistory.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/MerkleTreeWithHistory.sol b/contracts/MerkleTreeWithHistory.sol index 23412a2..68cea8a 100644 --- a/contracts/MerkleTreeWithHistory.sol +++ b/contracts/MerkleTreeWithHistory.sol @@ -58,7 +58,7 @@ contract MerkleTreeWithHistory { function _insert(uint256 leaf) internal { uint32 current_index = next_index; - require(current_index != 2**(levels - 1), "Merkle tree is full"); + require(current_index != 2**(levels - 1), "Merkle tree is full. No more leafs can be added"); next_index += 1; uint256 current_level_hash = leaf; uint256 left; diff --git a/test/MerkleTreeWithHistory.test.js b/test/MerkleTreeWithHistory.test.js index 742c00c..82d8481 100644 --- a/test/MerkleTreeWithHistory.test.js +++ b/test/MerkleTreeWithHistory.test.js @@ -185,10 +185,10 @@ contract('MerkleTreeWithHistory', accounts => { } let error = await merkleTreeWithHistory.insert(1337).should.be.rejected - error.reason.should.be.equal('Merkle tree is full') + error.reason.should.be.equal('Merkle tree is full. No more leafs can be added') error = await merkleTreeWithHistory.insert(1).should.be.rejected - error.reason.should.be.equal('Merkle tree is full') + error.reason.should.be.equal('Merkle tree is full. No more leafs can be added') }) it.skip('mimc gas', async () => { From 13e01755a6c66132e1f8cc99487acc1c5e5e396c Mon Sep 17 00:00:00 2001 From: Roman Storm Date: Fri, 13 Sep 2019 18:05:08 -0700 Subject: [PATCH 16/18] WIP --- cli.js | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 6 deletions(-) diff --git a/cli.js b/cli.js index bc88ceb..0f1740f 100755 --- a/cli.js +++ b/cli.js @@ -12,8 +12,8 @@ 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, ETH_AMOUNT, EMPTY_ELEMENT +let web3, mixer, erc20mixer, circuit, proving_key, groth16, erc20 +let MERKLE_TREE_HEIGHT, ETH_AMOUNT, EMPTY_ELEMENT, ERC20_TOKEN const inBrowser = (typeof window !== 'undefined') const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes)) @@ -37,6 +37,78 @@ async function deposit() { return note } +async function depositErc20() { + const account = (await web3.eth.getAccounts())[0] + const tokenAmount = process.env.TOKEN_AMOUNT + await erc20.methods.mint(account, tokenAmount).send({ from: account, gas:1e6 }) + const allowance = await erc20.methods.allowance(account, erc20mixer.address).call() + console.log('erc20mixer allowance', allowance.toString(10)) + + await erc20.methods.approve(erc20mixer.address, tokenAmount).send({ from: account, gas:1e6 }) + + const deposit = createDeposit(rbigint(31), rbigint(31)) + await erc20mixer.methods.deposit('0x' + deposit.commitment.toString(16)).send({ value: ETH_AMOUNT, from: account, gas:1e6 }) + + const balance = await erc20.methods.balanceOf(erc20mixer.address).call() + console.log('erc20mixer balance', balance.toString(10)) + const note = '0x' + deposit.preimage.toString('hex') + console.log('Your note:', note) + return note +} + +async function withdrawErc20(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 erc20mixer.getPastEvents('Deposit', { fromBlock: erc20mixer.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 erc20mixer.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 getBalance(receiver) { const balance = await web3.eth.getBalance(receiver) console.log('Balance is ', web3.utils.fromWei(balance)) @@ -96,7 +168,7 @@ async function withdraw(note, receiver) { } async function init() { - let contractJson + let contractJson, erc20ContractJson, erc20mixerJson if (inBrowser) { web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 }) contractJson = await (await fetch('build/contracts/ETHMixer.json')).json() @@ -114,12 +186,25 @@ async function init() { MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT ETH_AMOUNT = process.env.ETH_AMOUNT EMPTY_ELEMENT = process.env.EMPTY_ELEMENT + ERC20_TOKEN = process.env.ERC20_TOKEN + erc20ContractJson = require('./build/contracts/ERC20Mock.json') + erc20mixerJson = require('./build/contracts/ERC20Mixer.json') } 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 + // 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 + + const tx3 = await web3.eth.getTransaction(erc20mixerJson.networks[netId].transactionHash) + erc20mixer = new web3.eth.Contract(erc20mixerJson.abi, erc20mixerJson.networks[netId].address) + erc20mixer.deployedBlock = tx3.blockNumber + + if(ERC20_TOKEN === '') { + erc20 = new web3.eth.Contract(erc20ContractJson.abi, erc20ContractJson.networks[netId].address) + const tx2 = await web3.eth.getTransaction(erc20ContractJson.networks[netId].transactionHash) + erc20.deployedBlock = tx2.blockNumber + } console.log('Loaded') } @@ -167,6 +252,13 @@ if (inBrowser) { else printHelp(1) break + case 'depositErc20': + if (args.length === 1) { + init().then(() => depositErc20()).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)}) From 9132aeb6d5b557e4ed4ed2c52bdf1a6dea250fc4 Mon Sep 17 00:00:00 2001 From: Alexey Date: Mon, 16 Sep 2019 13:07:14 +0300 Subject: [PATCH 17/18] max leaves count fix --- README.md | 6 +++--- contracts/MerkleTreeWithHistory.sol | 2 +- test/MerkleTreeWithHistory.test.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6920fc9..40ecc08 100644 --- a/README.md +++ b/README.md @@ -70,13 +70,13 @@ If you want, you can point the app to existing tornado contracts on Mainnet or K ## Deploy ETH Tornado Cash 1. `cp .env.example .env` 1. Tune all necessary params -1. `npx truffle migrate --f 2 --to 4` +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 --f 2 --to 3` -1. `npx truffle migrate --f 5` +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 mixers, then after you deployed one of the mixers you should only run 4th or 5th migration for ETH or ERC20 mixers respectively (`--f 4 --to 4` or `--f 5`). diff --git a/contracts/MerkleTreeWithHistory.sol b/contracts/MerkleTreeWithHistory.sol index 68cea8a..f75311a 100644 --- a/contracts/MerkleTreeWithHistory.sol +++ b/contracts/MerkleTreeWithHistory.sol @@ -58,7 +58,7 @@ contract MerkleTreeWithHistory { function _insert(uint256 leaf) internal { uint32 current_index = next_index; - require(current_index != 2**(levels - 1), "Merkle tree is full. No more leafs can be added"); + require(current_index != 2**levels, "Merkle tree is full. No more leafs can be added"); next_index += 1; uint256 current_level_hash = leaf; uint256 left; diff --git a/test/MerkleTreeWithHistory.test.js b/test/MerkleTreeWithHistory.test.js index 82d8481..9b39516 100644 --- a/test/MerkleTreeWithHistory.test.js +++ b/test/MerkleTreeWithHistory.test.js @@ -180,7 +180,7 @@ contract('MerkleTreeWithHistory', accounts => { zeroValue = 1337 merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, zeroValue) - for (let i = 0; i < 2**(levels - 1); i++) { + for (let i = 0; i < 2**levels; i++) { await merkleTreeWithHistory.insert(i+42).should.be.fulfilled } From 9e7aa186dc64a62af558228c4416248e5d8820e6 Mon Sep 17 00:00:00 2001 From: Alexey Date: Mon, 16 Sep 2019 14:14:49 +0300 Subject: [PATCH 18/18] readme bug --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 40ecc08..99de881 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Use with command line version with Ganache: ### ERC20Mixer 1. `npm run migrate:dev` 1. `./cli.js depositErc20` -1. `./cli.js withdraw ` +1. `./cli.js withdrawErc20 ` 1. `./cli.js balanceErc20 ` If you want, you can point the app to existing tornado contracts on Mainnet or Kovan, it should work without any changes