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