Merge pull request #9 from peppersec/erc20_support

Erc20 support
This commit is contained in:
Roman Storm 2019-09-16 10:43:17 -07:00 committed by GitHub
commit 5ef6e33c78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 902 additions and 109 deletions

View File

@ -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

2
.gitignore vendored
View File

@ -94,3 +94,5 @@ typings/
# DynamoDB Local files
.dynamodb/
ERC20Mixer_flat.sol
ETHMixer_flat.sol

View File

@ -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 <note from previous step> <destination eth address>`
1. `./cli.js balance <destination eth address>`
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 <note from previous step> <destination eth address>`
1. `./cli.js balance <destination eth address>`
### ERC20Mixer
1. `npm run migrate:dev`
1. `./cli.js depositErc20`
1. `./cli.js withdrawErc20 <note from previous step> <destination eth address> <relayer eth address>`
1. `./cli.js balanceErc20 <destination eth address> <relayer eth address>`
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,

View File

@ -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);

165
cli.js
View File

@ -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<void>}
*/
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 () => {

89
contracts/ERC20Mixer.sol Normal file
View File

@ -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");
}
}
}

36
contracts/ETHMixer.sol Normal file
View File

@ -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");
}
}

View File

@ -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;

View File

@ -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 {}
}

View File

@ -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 {
}
}

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

@ -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);
}

View File

@ -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)
})
}

View File

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

View File

@ -0,0 +1,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)
})
}

123
package-lock.json generated
View File

@ -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",

View File

@ -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",

354
test/ERC20Mixer.test.js Normal file
View File

@ -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,
)
})
})

View File

@ -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,

View File

@ -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)
})
})

View File

@ -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: {