diff --git a/.env.example b/.env.example index be24a76..7fdb12d 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,16 @@ MERKLE_TREE_HEIGHT=16 # in wei -AMOUNT=1000000000000000000 +ETH_AMOUNT=100000000000000000 +TOKEN_AMOUNT=100000000000000000 EMPTY_ELEMENT=1 PRIVATE_KEY= +ERC20_TOKEN= + +# DAI mirror in Kovan +#ERC20_TOKEN=0xd2b1a6b34f4a68425e7c28b4db5a37be3b7a4947 +# the block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some DAI is 13146218 + +# USDT mirror in Kovan +#ERC20_TOKEN=0xf3e0d7bf58c5d455d31ef1c2d5375904df525105 +#TOKEN_AMOUNT=1000000 +# the block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some USDT is 13147586 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 7eb3a8e..99de881 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,6 @@ You can see example usage in cli.js, it works both in console and in browser. 1. `npx ganache-cli` 1. `npm run test` - optionally run tests. It may fail for the first time, just run one more time. -Use with command line version with Ganache: - -1. `npm run migrate:dev` -1. `./cli.js deposit` -1. `./cli.js withdraw ` -1. `./cli.js balance ` - Use browser version on Kovan: 1. `vi .env` - add your Kovan private key to deploy contracts @@ -59,8 +52,34 @@ Use browser version on Kovan: 1. `npx http-server` - serve current dir, you can use any other static http server 1. Open `localhost:8080` +Use with command line version with Ganache: +### ETHMixer +1. `npm run migrate:dev` +1. `./cli.js deposit` +1. `./cli.js withdraw ` +1. `./cli.js balance ` + +### ERC20Mixer +1. `npm run migrate:dev` +1. `./cli.js depositErc20` +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 +## Deploy ETH Tornado Cash +1. `cp .env.example .env` +1. Tune all necessary params +1. `npx truffle migrate --network kovan --reset --f 2 --to 4` + +## Deploy ERC20 Tornado Cash +1. `cp .env.example .env` +1. Tune all necessary params +1. `npx truffle migrate --network kovan --reset --f 2 --to 3` +1. `npx truffle migrate --network kovan --reset --f 5` + +**Note**. If you want to reuse the same verifier for all the 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/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 06aed95..dd4ee1f 100755 --- a/cli.js +++ b/cli.js @@ -12,8 +12,9 @@ const Web3 = require('web3') const buildGroth16 = require('websnark/src/groth16') const websnarkUtils = require('websnark/src/utils') -let web3, mixer, circuit, proving_key, groth16 -let MERKLE_TREE_HEIGHT, AMOUNT, EMPTY_ELEMENT +let web3, mixer, erc20mixer, circuit, proving_key, groth16, erc20 +let MERKLE_TREE_HEIGHT, ETH_AMOUNT, EMPTY_ELEMENT, ERC20_TOKEN +const inBrowser = (typeof window !== 'undefined') /** Generate random number of specified byte length */ const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes)) @@ -39,19 +40,103 @@ 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) return note } -/** - * Make a withdrawal - * @param note A preimage containing secret and nullifier - * @param receiver Address for receiving funds - * @returns {Promise} - */ +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 }) + + await erc20.methods.approve(erc20mixer.address, tokenAmount).send({ from: account, gas:1e6 }) + const allowance = await erc20.methods.allowance(account, erc20mixer.address).call() + console.log('erc20mixer allowance', allowance.toString(10)) + + 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, relayer) { + 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 erc20mixer.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), + relayer: bigInt(relayer), + fee: bigInt(web3.utils.toWei('0.01')), + + // 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 erc20mixer.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)) +} + +async function getBalanceErc20(receiver, relayer) { + const balanceReceiver = await web3.eth.getBalance(receiver) + const balanceRelayer = await web3.eth.getBalance(relayer) + const tokenBalanceReceiver = await erc20.methods.balanceOf(receiver).call() + const tokenBalanceRelayer = await erc20.methods.balanceOf(relayer).call() + console.log('Receiver eth Balance is ', web3.utils.fromWei(balanceReceiver)) + console.log('Relayer eth Balance is ', web3.utils.fromWei(balanceRelayer)) + + console.log('Receiver token Balance is ', web3.utils.fromWei(tokenBalanceReceiver.toString())) + console.log('Relayer token Balance is ', web3.utils.fromWei(tokenBalanceRelayer.toString())) +} + async function withdraw(note, receiver) { // Decode hex string and restore the deposit object let buf = Buffer.from(note.slice(2), 'hex') @@ -88,6 +173,7 @@ async function withdraw(note, receiver) { root: root, nullifierHash, receiver: bigInt(receiver), + relayer: bigInt(0), fee: bigInt(0), // Private snark inputs @@ -108,47 +194,52 @@ async function withdraw(note, receiver) { console.log('Done') } -/** - * Get default wallet balance - */ -async function getBalance(receiver) { - const balance = await web3.eth.getBalance(receiver) - console.log('Balance is ', web3.utils.fromWei(balance)) -} - -const inBrowser = (typeof window !== 'undefined') - /** * Init web3, contracts, and snark */ async function init() { - let contractJson + let contractJson, erc20ContractJson, erc20mixerJson if (inBrowser) { // Initialize using injected web3 (Metamask) // To assemble web version run `npm run browserify` web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 }) - contractJson = await (await fetch('build/contracts/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 - AMOUNT = 1e18 + ETH_AMOUNT = 1e18 EMPTY_ELEMENT = 1 } else { // Initialize from local node 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() MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT - AMOUNT = process.env.AMOUNT + 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 + if (contractJson.networks[netId]) { + 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') } @@ -196,12 +287,25 @@ 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)}) } else printHelp(1) break + case 'balanceErc20': + if (args.length === 3 && /^0x[0-9a-fA-F]{40}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) { + init().then(() => getBalanceErc20(args[1], args[2])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)}) + } else + printHelp(1) + break case 'withdraw': if (args.length === 3 && /^0x[0-9a-fA-F]{124}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) { init().then(() => withdraw(args[1], args[2])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)}) @@ -209,6 +313,13 @@ if (inBrowser) { else printHelp(1) break + case 'withdrawErc20': + if (args.length === 4 && /^0x[0-9a-fA-F]{124}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2]) && /^0x[0-9a-fA-F]{40}$/.test(args[3])) { + init().then(() => withdrawErc20(args[1], args[2], args[3])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)}) + } + else + printHelp(1) + break case 'auto': if (args.length === 1) { (async () => { diff --git a/contracts/ERC20Mixer.sol b/contracts/ERC20Mixer.sol new file mode 100644 index 0000000..8a815c5 --- /dev/null +++ b/contracts/ERC20Mixer.sol @@ -0,0 +1,89 @@ +// 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 ERC20Mixer is Mixer { + address public token; + // ether value to cover network fee (for relayer) and to have some ETH on a brand new address + uint256 public userEther; + + constructor( + address _verifier, + uint256 _userEther, + uint8 _merkleTreeHeight, + uint256 _emptyElement, + address payable _operator, + address _token, + uint256 _mixDenomination + ) Mixer(_verifier, _mixDenomination, _merkleTreeHeight, _emptyElement, _operator) public { + token = _token; + userEther = _userEther; + } + + function _processDeposit() internal { + require(msg.value == userEther, "Please send `userEther` ETH along with transaction"); + safeErc20TransferFrom(msg.sender, address(this), mixDenomination); + } + + function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee) internal { + _receiver.transfer(userEther); + + safeErc20Transfer(_receiver, mixDenomination - _fee); + if (_fee > 0) { + safeErc20Transfer(_relayer, _fee); + } + } + + function safeErc20TransferFrom(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 contract returns some data let's make sure that is `true` according to standard + if (data.length > 0) { + assembly { + success := mload(add(data, 0x20)) + } + require(success, "not enough allowed tokens"); + } + } + + function safeErc20Transfer(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 contract returns some data let's make sure that is `true` according to standard + if (data.length > 0) { + assembly { + success := mload(add(data, 0x20)) + } + require(success, "not enough tokens"); + } + } +} diff --git a/contracts/ETHMixer.sol b/contracts/ETHMixer.sol new file mode 100644 index 0000000..107fe6c --- /dev/null +++ b/contracts/ETHMixer.sol @@ -0,0 +1,36 @@ +// 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 _mixDenomination, + uint8 _merkleTreeHeight, + uint256 _emptyElement, + address payable _operator + ) Mixer(_verifier, _mixDenomination, _merkleTreeHeight, _emptyElement, _operator) public { + } + + function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee) internal { + _receiver.transfer(mixDenomination - _fee); + if (_fee > 0) { + _relayer.transfer(_fee); + } + } + + function _processDeposit() internal { + require(msg.value == mixDenomination, "Please send `mixDenomination` ETH along with transaction"); + } +} diff --git a/contracts/MerkleTreeWithHistory.sol b/contracts/MerkleTreeWithHistory.sol index 23412a2..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"); + 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/contracts/Mixer.sol b/contracts/Mixer.sol index 9a114af..262967c 100644 --- a/contracts/Mixer.sol +++ b/contracts/Mixer.sol @@ -14,11 +14,10 @@ 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 { - uint256 public transferValue; bool public isDepositsEnabled = true; // operator can disable new deposits in case of emergency // it also receives a relayer fee @@ -27,40 +26,47 @@ 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); + event Withdraw(address to, uint256 nullifierHash, address indexed relayer, uint256 fee); /** @dev The constructor @param _verifier the address of SNARK verifier for this contract - @param _transferValue the value for all deposits in this contract in wei + @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, + uint256 _mixDenomination, uint8 _merkleTreeHeight, uint256 _emptyElement, address payable _operator ) MerkleTreeWithHistory(_merkleTreeHeight, _emptyElement) public { verifier = IVerifier(_verifier); - transferValue = _transferValue; operator = _operator; + mixDenomination = _mixDenomination; } - /** - @dev Deposit funds into mixer. The caller must send value equal to `transferValue` of this mixer. + @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(msg.value == transferValue, "Please send `transferValue` ETH along with transaction"); require(!commitments[commitment], "The commitment has been submitted"); + _processDeposit(); _insert(commitment); commitments[commitment] = true; + emit Deposit(commitment, next_index - 1, block.timestamp); } - /** @dev Withdraw deposit from the mixer. `a`, `b`, and `c` are zkSNARK proof data, and input is an array of circuit public inputs `input` array consists of: @@ -69,23 +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(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); + _processWithdraw(receiver, relayer, fee); + emit Withdraw(receiver, nullifierHash, relayer, fee); } function toggleDeposits() external { @@ -101,4 +104,8 @@ contract Mixer is MerkleTreeWithHistory { function isSpent(uint256 nullifier) public view returns(bool) { return nullifierHashes[nullifier]; } + + function _processDeposit() internal {} + function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee) internal {} + } 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/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/4_deploy_eth_mixer.js b/migrations/4_deploy_eth_mixer.js new file mode 100644 index 0000000..3626019 --- /dev/null +++ b/migrations/4_deploy_eth_mixer.js @@ -0,0 +1,17 @@ +/* global artifacts */ +require('dotenv').config({ path: '../.env' }) +const ETHMixer = artifacts.require('ETHMixer') +const Verifier = artifacts.require('Verifier') +const MiMC = artifacts.require('MiMC') + + +module.exports = function(deployer, network, accounts) { + return deployer.then(async () => { + 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, ETH_AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, accounts[0]) + console.log('ETHMixer\'s address ', mixer.address) + }) +} diff --git a/migrations/4_deploy_mixer.js b/migrations/4_deploy_mixer.js deleted file mode 100644 index a1345c7..0000000 --- a/migrations/4_deploy_mixer.js +++ /dev/null @@ -1,17 +0,0 @@ -/* global artifacts */ -require('dotenv').config({ path: '../.env' }) -const Mixer = artifacts.require('Mixer') -const Verifier = artifacts.require('Verifier') -const MiMC = artifacts.require('MiMC') - - -module.exports = function(deployer, network, accounts) { - return deployer.then(async () => { - const { MERKLE_TREE_HEIGHT, AMOUNT, EMPTY_ELEMENT } = process.env - const verifier = await Verifier.deployed() - const miMC = await MiMC.deployed() - await Mixer.link(MiMC, miMC.address) - const mixer = await deployer.deploy(Mixer, verifier.address, AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, accounts[0]) - console.log('Mixer\'s address ', mixer.address) - }) -} diff --git a/migrations/5_deploy_erc20_mixer.js b/migrations/5_deploy_erc20_mixer.js new file mode 100644 index 0000000..8ee907c --- /dev/null +++ b/migrations/5_deploy_erc20_mixer.js @@ -0,0 +1,32 @@ +/* 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, 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) + let token = ERC20_TOKEN + if(token === '') { + const tokenInstance = await deployer.deploy(ERC20Mock) + token = tokenInstance.address + } + const mixer = await deployer.deploy( + ERC20Mixer, + verifier.address, + ETH_AMOUNT, + MERKLE_TREE_HEIGHT, + EMPTY_ELEMENT, + accounts[0], + token, + TOKEN_AMOUNT + ) + console.log('ERC20Mixer\'s address ', mixer.address) + }) +} diff --git a/package-lock.json b/package-lock.json index 429b085..bd46e26 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", @@ -2357,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", @@ -2367,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": { @@ -2422,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": { @@ -9867,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 86c326f..34daa2e 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,13 @@ "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": "", "license": "ISC", "dependencies": { + "@openzeppelin/contracts": "^2.3.0", "bn-chai": "^1.0.1", "browserify": "^16.3.0", "chai": "^4.2.0", @@ -29,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", diff --git a/test/ERC20Mixer.test.js b/test/ERC20Mixer.test.js new file mode 100644 index 0000000..8c69835 --- /dev/null +++ b/test/ERC20Mixer.test.js @@ -0,0 +1,354 @@ +/* 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 } = require('web3-utils') +const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper') + +const Mixer = artifacts.require('./ERC20Mixer.sol') +const Token = artifacts.require('./ERC20Mock.sol') +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') +const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts +const snarkjs = require('snarkjs') +const bigInt = snarkjs.bigInt +const crypto = require('crypto') +const circomlib = require('circomlib') +const MerkleTree = require('../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 +} + +function getRandomReceiver() { + let receiver = rbigint(20) + while (toHex(receiver.toString()).length !== 42) { + receiver = rbigint(20) + } + return receiver +} + +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 + let tokenDenomination = TOKEN_AMOUNT || '1000000000000000000' // 1 ether + const value = ETH_AMOUNT || '1000000000000000000' // 1 ether + let snapshotId + let prefix = 'test' + let tree + const fee = bigInt(ETH_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() + 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') + 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('should work', async () => { + const commitment = 43 + await token.approve(mixer.address, tokenDenomination) + + let { logs } = await mixer.deposit(commitment, { value, from: sender }) + + logs[0].event.should.be.equal('Deposit') + logs[0].args.commitment.should.be.eq.BN(toBN(commitment)) + logs[0].args.leafIndex.should.be.eq.BN(toBN(0)) + }) + }) + + describe('#withdraw', () => { + it('should work', async () => { + const deposit = generateDeposit() + const user = accounts[4] + await tree.insert(deposit.commitment) + await token.mint(user, tokenDenomination) + + const balanceUserBefore = await token.balanceOf(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()), { value, from: user, gasPrice: '0' }) + + 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)), + relayer, + 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' }) + + 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).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(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) + }) + + 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)), + relayer: operator, + 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.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) + }) + it.skip('should work with REAL DAI', async () => { + // dont forget to specify your token in .env + // and send `tokenDenomination` to accounts[0] (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1) + // run ganache as + // npx ganache-cli --fork https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448@13146218 -d --keepAliveTimeout 20 + const deposit = generateDeposit() + const user = accounts[4] + const userBal = await token.balanceOf(user) + console.log('userBal', userBal.toString()) + const senderBal = await token.balanceOf(sender) + console.log('senderBal', senderBal.toString()) + 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)), + relayer: operator, + 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.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) + }) + }) + + 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 96% rename from test/Mixer.test.js rename to test/ETHMixer.test.js index 1d18520..cd6753b 100644 --- a/test/Mixer.test.js +++ b/test/ETHMixer.test.js @@ -8,8 +8,8 @@ const fs = require('fs') const { toBN, toHex, randomHex } = require('web3-utils') const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper') -const Mixer = artifacts.require('./Mixer.sol') -const { AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env +const Mixer = artifacts.require('./ETHMixer.sol') +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.mixDenomination() + etherDenomination.should.be.eq.BN(toBN(value)) }) }) @@ -141,6 +141,7 @@ contract('Mixer', accounts => { root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifier: deposit.nullifier, + relayer: operator, receiver, fee, secret: deposit.secret, @@ -196,6 +197,7 @@ contract('Mixer', accounts => { // public root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), + relayer: operator, receiver, fee, @@ -235,6 +237,7 @@ contract('Mixer', 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('Mixer', accounts => { root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifier: deposit.nullifier, + relayer: operator, receiver, fee, secret: deposit.secret, @@ -275,6 +279,7 @@ contract('Mixer', accounts => { root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifier: deposit.nullifier, + relayer: operator, receiver, fee, secret: deposit.secret, @@ -299,6 +304,7 @@ contract('Mixer', accounts => { root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifier: deposit.nullifier, + relayer: operator, receiver, fee: oneEtherFee, secret: deposit.secret, @@ -323,6 +329,7 @@ contract('Mixer', accounts => { nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), root, nullifier: deposit.nullifier, + relayer: operator, receiver, fee, secret: deposit.secret, @@ -350,6 +357,7 @@ contract('Mixer', accounts => { root, nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifier: deposit.nullifier, + relayer: operator, receiver, fee, secret: deposit.secret, diff --git a/test/MerkleTreeWithHistory.test.js b/test/MerkleTreeWithHistory.test.js index 5d0b2d8..9b39516 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 @@ -180,15 +180,24 @@ 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 } 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 () => { + levels = 6 + zeroValue = 1337 + merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels, zeroValue) + + const gas = await merkleTreeWithHistory.hashLeftRight.estimateGas(zeroValue, zeroValue) + console.log('gas', gas - 21000) }) }) diff --git a/truffle-config.js b/truffle-config.js index a9078e8..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, @@ -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: {