Merge pull request #19 from peppersec/audit-3

Audit fixes (batch 3)
This commit is contained in:
Roman Semenov 2019-11-08 13:30:25 +03:00 committed by GitHub
commit bc8d0b20fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 3745 additions and 6716 deletions

View file

@ -29,7 +29,7 @@ template CommitmentHasher() {
template Withdraw(levels) { template Withdraw(levels) {
signal input root; signal input root;
signal input nullifierHash; signal input nullifierHash;
signal input receiver; // not taking part in any computations signal input recipient; // not taking part in any computations
signal input relayer; // not taking part in any computations signal input relayer; // not taking part in any computations
signal input fee; // not taking part in any computations signal input fee; // not taking part in any computations
signal input refund; // not taking part in any computations signal input refund; // not taking part in any computations

69
cli.js
View file

@ -66,7 +66,7 @@ async function depositErc20() {
return note return note
} }
async function withdrawErc20(note, receiver, relayer) { async function withdrawErc20(note, recipient, relayer) {
let buf = Buffer.from(note.slice(2), 'hex') let buf = Buffer.from(note.slice(2), 'hex')
let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62))) let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62)))
@ -98,7 +98,7 @@ async function withdrawErc20(note, receiver, relayer) {
// public // public
root: root, root: root,
nullifierHash, nullifierHash,
receiver: bigInt(receiver), recipient: bigInt(recipient),
relayer: bigInt(relayer), relayer: bigInt(relayer),
fee: bigInt(web3.utils.toWei('0.01')), fee: bigInt(web3.utils.toWei('0.01')),
refund: bigInt(0), refund: bigInt(0),
@ -113,32 +113,47 @@ async function withdrawErc20(note, receiver, relayer) {
console.log('Generating SNARK proof') console.log('Generating SNARK proof')
console.time('Proof time') console.time('Proof time')
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) const { proof } = websnarkUtils.toSolidityInput(proofData)
console.timeEnd('Proof time') console.timeEnd('Proof time')
console.log('Submitting withdraw transaction') console.log('Submitting withdraw transaction')
await erc20mixer.methods.withdraw(proof, publicSignals).send({ from: (await web3.eth.getAccounts())[0], gas: 1e6 }) const args = [
toHex(input.root),
toHex(input.nullifierHash),
toHex(input.recipient, 20),
toHex(input.relayer, 20),
toHex(input.fee),
toHex(input.refund)
]
await erc20mixer.methods.withdraw(proof, ...args).send({ from: (await web3.eth.getAccounts())[0], gas: 1e6 })
console.log('Done') console.log('Done')
} }
async function getBalance(receiver) { async function getBalance(recipient) {
const balance = await web3.eth.getBalance(receiver) const balance = await web3.eth.getBalance(recipient)
console.log('Balance is ', web3.utils.fromWei(balance)) console.log('Balance is ', web3.utils.fromWei(balance))
} }
async function getBalanceErc20(receiver, relayer) { async function getBalanceErc20(recipient, relayer) {
const balanceReceiver = await web3.eth.getBalance(receiver) const balanceRecipient = await web3.eth.getBalance(recipient)
const balanceRelayer = await web3.eth.getBalance(relayer) const balanceRelayer = await web3.eth.getBalance(relayer)
const tokenBalanceReceiver = await erc20.methods.balanceOf(receiver).call() const tokenBalanceRecipient = await erc20.methods.balanceOf(recipient).call()
const tokenBalanceRelayer = await erc20.methods.balanceOf(relayer).call() const tokenBalanceRelayer = await erc20.methods.balanceOf(relayer).call()
console.log('Receiver eth Balance is ', web3.utils.fromWei(balanceReceiver)) console.log('Recipient eth Balance is ', web3.utils.fromWei(balanceRecipient))
console.log('Relayer eth Balance is ', web3.utils.fromWei(balanceRelayer)) console.log('Relayer eth Balance is ', web3.utils.fromWei(balanceRelayer))
console.log('Receiver token Balance is ', web3.utils.fromWei(tokenBalanceReceiver.toString())) console.log('Recipient token Balance is ', web3.utils.fromWei(tokenBalanceRecipient.toString()))
console.log('Relayer token Balance is ', web3.utils.fromWei(tokenBalanceRelayer.toString())) console.log('Relayer token Balance is ', web3.utils.fromWei(tokenBalanceRelayer.toString()))
} }
async function withdraw(note, receiver) { function toHex(number, length = 32) {
let str = bigInt(number).toString(16)
while (str.length < length * 2) str = '0' + str
str = '0x' + str
return str
}
async function withdraw(note, recipient) {
// Decode hex string and restore the deposit object // Decode hex string and restore the deposit object
let buf = Buffer.from(note.slice(2), 'hex') let buf = Buffer.from(note.slice(2), 'hex')
let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62))) let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62)))
@ -150,16 +165,16 @@ async function withdraw(note, receiver) {
console.log('Getting current state from mixer contract') console.log('Getting current state from mixer contract')
const events = await mixer.getPastEvents('Deposit', { fromBlock: mixer.deployedBlock, toBlock: 'latest' }) const events = await mixer.getPastEvents('Deposit', { fromBlock: mixer.deployedBlock, toBlock: 'latest' })
const leaves = events const leaves = events
.sort((a, b) => a.returnValues.leafIndex.sub(b.returnValues.leafIndex)) // Sort events in chronological order .sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order
.map(e => e.returnValues.commitment) .map(e => e.returnValues.commitment)
const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves) const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves)
// Find current commitment in the tree // Find current commitment in the tree
let depositEvent = events.find(e => e.returnValues.commitment.eq(paddedCommitment)) let depositEvent = events.find(e => e.returnValues.commitment === paddedCommitment)
let leafIndex = depositEvent ? depositEvent.returnValues.leafIndex.toNumber() : -1 let leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
// Validate that our data is correct // Validate that our data is correct
const isValidRoot = await mixer.methods.isKnownRoot(await tree.root()).call() const isValidRoot = await mixer.methods.isKnownRoot(toHex(await tree.root())).call()
const isSpent = await mixer.methods.isSpent(paddedNullifierHash).call() const isSpent = await mixer.methods.isSpent(paddedNullifierHash).call()
assert(isValidRoot === true) // Merkle tree assembled correctly assert(isValidRoot === true) // Merkle tree assembled correctly
assert(isSpent === false) // The note is not spent assert(isSpent === false) // The note is not spent
@ -173,7 +188,7 @@ async function withdraw(note, receiver) {
// Public snark inputs // Public snark inputs
root: root, root: root,
nullifierHash, nullifierHash,
receiver: bigInt(receiver), recipient: bigInt(recipient),
relayer: bigInt(0), relayer: bigInt(0),
fee: bigInt(0), fee: bigInt(0),
refund: bigInt(0), refund: bigInt(0),
@ -188,11 +203,19 @@ async function withdraw(note, receiver) {
console.log('Generating SNARK proof') console.log('Generating SNARK proof')
console.time('Proof time') console.time('Proof time')
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) const { proof } = websnarkUtils.toSolidityInput(proofData)
console.timeEnd('Proof time') console.timeEnd('Proof time')
console.log('Submitting withdraw transaction') console.log('Submitting withdraw transaction')
await mixer.methods.withdraw(proof, publicSignals).send({ from: (await web3.eth.getAccounts())[0], gas: 1e6 }) const args = [
toHex(input.root),
toHex(input.nullifierHash),
toHex(input.recipient, 20),
toHex(input.relayer, 20),
toHex(input.fee),
toHex(input.refund)
]
await mixer.methods.withdraw(proof, ...args).send({ from: (await web3.eth.getAccounts())[0], gas: 1e6 })
console.log('Done') console.log('Done')
} }
@ -250,8 +273,8 @@ function printHelp(code = 0) {
Submit a deposit from default eth account and return the resulting note Submit a deposit from default eth account and return the resulting note
$ ./cli.js deposit $ ./cli.js deposit
Withdraw a note to 'receiver' account Withdraw a note to 'recipient' account
$ ./cli.js withdraw <note> <receiver> $ ./cli.js withdraw <note> <recipient>
Check address balance Check address balance
$ ./cli.js balance <address> $ ./cli.js balance <address>
@ -270,8 +293,8 @@ if (inBrowser) {
window.deposit = deposit window.deposit = deposit
window.withdraw = async () => { window.withdraw = async () => {
const note = prompt('Enter the note to withdraw') const note = prompt('Enter the note to withdraw')
const receiver = (await web3.eth.getAccounts())[0] const recipient = (await web3.eth.getAccounts())[0]
await withdraw(note, receiver) await withdraw(note, recipient)
} }
init() init()
} else { } else {

View file

@ -19,7 +19,7 @@ contract ERC20Mixer is Mixer {
constructor( constructor(
IVerifier _verifier, IVerifier _verifier,
uint256 _denomination, uint256 _denomination,
uint8 _merkleTreeHeight, uint32 _merkleTreeHeight,
address _operator, address _operator,
address _token address _token
) Mixer(_verifier, _denomination, _merkleTreeHeight, _operator) public { ) Mixer(_verifier, _denomination, _merkleTreeHeight, _operator) public {
@ -31,15 +31,15 @@ contract ERC20Mixer is Mixer {
_safeErc20TransferFrom(msg.sender, address(this), denomination); _safeErc20TransferFrom(msg.sender, address(this), denomination);
} }
function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee, uint256 _refund) internal { function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) internal {
require(msg.value == _refund, "Incorrect refund amount received by the contract"); require(msg.value == _refund, "Incorrect refund amount received by the contract");
_safeErc20Transfer(_receiver, denomination - _fee); _safeErc20Transfer(_recipient, denomination - _fee);
if (_fee > 0) { if (_fee > 0) {
_safeErc20Transfer(_relayer, _fee); _safeErc20Transfer(_relayer, _fee);
} }
if (_refund > 0) { if (_refund > 0) {
_receiver.transfer(_refund); _recipient.transfer(_refund);
} }
} }
@ -51,7 +51,7 @@ contract ERC20Mixer is Mixer {
if (data.length > 0) { if (data.length > 0) {
require(data.length == 32, "data length should be either 0 or 32 bytes"); require(data.length == 32, "data length should be either 0 or 32 bytes");
success = abi.decode(data, (bool)); success = abi.decode(data, (bool));
require(success, "not enough allowed tokens"); require(success, "not enough allowed tokens. Token returns false.");
} }
} }
@ -63,7 +63,7 @@ contract ERC20Mixer is Mixer {
if (data.length > 0) { if (data.length > 0) {
require(data.length == 32, "data length should be either 0 or 32 bytes"); require(data.length == 32, "data length should be either 0 or 32 bytes");
success = abi.decode(data, (bool)); success = abi.decode(data, (bool));
require(success, "not enough tokens"); require(success, "not enough tokens. Token returns false.");
} }
} }
} }

View file

@ -17,7 +17,7 @@ contract ETHMixer is Mixer {
constructor( constructor(
IVerifier _verifier, IVerifier _verifier,
uint256 _denomination, uint256 _denomination,
uint8 _merkleTreeHeight, uint32 _merkleTreeHeight,
address _operator address _operator
) Mixer(_verifier, _denomination, _merkleTreeHeight, _operator) public { ) Mixer(_verifier, _denomination, _merkleTreeHeight, _operator) public {
} }
@ -26,12 +26,12 @@ contract ETHMixer is Mixer {
require(msg.value == denomination, "Please send `mixDenomination` ETH along with transaction"); require(msg.value == denomination, "Please send `mixDenomination` ETH along with transaction");
} }
function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee, uint256 _refund) internal { function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) internal {
// sanity checks // sanity checks
require(msg.value == 0, "Message value is supposed to be zero for ETH mixer"); require(msg.value == 0, "Message value is supposed to be zero for ETH mixer");
require(_refund == 0, "Refund value is supposed to be zero for ETH mixer"); require(_refund == 0, "Refund value is supposed to be zero for ETH mixer");
_receiver.transfer(denomination - _fee); _recipient.transfer(denomination - _fee);
if (_fee > 0) { if (_fee > 0) {
_relayer.transfer(_fee); _relayer.transfer(_fee);
} }

View file

@ -19,26 +19,27 @@ contract MerkleTreeWithHistory {
uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617; uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
uint256 public constant ZERO_VALUE = 5702960885942360421128284892092891246826997279710054143430547229469817701242; // = MiMC("tornado") uint256 public constant ZERO_VALUE = 5702960885942360421128284892092891246826997279710054143430547229469817701242; // = MiMC("tornado")
uint256 public levels; uint32 public levels;
// the following variables are made public for easier testing and debugging and // the following variables are made public for easier testing and debugging and
// are not supposed to be accessed in regular code // are not supposed to be accessed in regular code
uint256 public constant ROOT_HISTORY_SIZE = 100; bytes32[] public filledSubtrees;
uint256[ROOT_HISTORY_SIZE] public roots; bytes32[] public zeros;
uint256 public currentRootIndex = 0; uint32 public currentRootIndex = 0;
uint32 public nextIndex = 0; uint32 public nextIndex = 0;
uint256[] public filledSubtrees; uint32 public constant ROOT_HISTORY_SIZE = 100;
uint256[] public zeros; bytes32[ROOT_HISTORY_SIZE] public roots;
constructor(uint256 _treeLevels) public { constructor(uint32 _treeLevels) public {
require(_treeLevels > 0, "_treeLevels should be greater than zero"); require(_treeLevels > 0, "_treeLevels should be greater than zero");
require(_treeLevels < 32, "_treeLevels should be less than 32");
levels = _treeLevels; levels = _treeLevels;
uint256 currentZero = ZERO_VALUE; bytes32 currentZero = bytes32(ZERO_VALUE);
zeros.push(currentZero); zeros.push(currentZero);
filledSubtrees.push(currentZero); filledSubtrees.push(currentZero);
for (uint8 i = 1; i < levels; i++) { for (uint32 i = 1; i < levels; i++) {
currentZero = hashLeftRight(currentZero, currentZero); currentZero = hashLeftRight(currentZero, currentZero);
zeros.push(currentZero); zeros.push(currentZero);
filledSubtrees.push(currentZero); filledSubtrees.push(currentZero);
@ -50,26 +51,26 @@ contract MerkleTreeWithHistory {
/** /**
@dev Hash 2 tree leaves, returns MiMC(_left, _right) @dev Hash 2 tree leaves, returns MiMC(_left, _right)
*/ */
function hashLeftRight(uint256 _left, uint256 _right) public pure returns (uint256) { function hashLeftRight(bytes32 _left, bytes32 _right) public pure returns (bytes32) {
require(_left < FIELD_SIZE, "_left should be inside the field"); require(uint256(_left) < FIELD_SIZE, "_left should be inside the field");
require(_right < FIELD_SIZE, "_right should be inside the field"); require(uint256(_right) < FIELD_SIZE, "_right should be inside the field");
uint256 R = _left; uint256 R = uint256(_left);
uint256 C = 0; uint256 C = 0;
(R, C) = Hasher.MiMCSponge(R, C, 0); (R, C) = Hasher.MiMCSponge(R, C, 0);
R = addmod(R, _right, FIELD_SIZE); R = addmod(R, uint256(_right), FIELD_SIZE);
(R, C) = Hasher.MiMCSponge(R, C, 0); (R, C) = Hasher.MiMCSponge(R, C, 0);
return R; return bytes32(R);
} }
function _insert(uint256 _leaf) internal returns(uint256 index) { function _insert(bytes32 _leaf) internal returns(uint32 index) {
uint32 currentIndex = nextIndex; uint32 currentIndex = nextIndex;
require(currentIndex != 2**levels, "Merkle tree is full. No more leafs can be added"); require(currentIndex != uint32(2)**levels, "Merkle tree is full. No more leafs can be added");
nextIndex += 1; nextIndex += 1;
uint256 currentLevelHash = _leaf; bytes32 currentLevelHash = _leaf;
uint256 left; bytes32 left;
uint256 right; bytes32 right;
for (uint256 i = 0; i < levels; i++) { for (uint32 i = 0; i < levels; i++) {
if (currentIndex % 2 == 0) { if (currentIndex % 2 == 0) {
left = currentLevelHash; left = currentLevelHash;
right = zeros[i]; right = zeros[i];
@ -93,31 +94,27 @@ contract MerkleTreeWithHistory {
/** /**
@dev Whether the root is present in the root history @dev Whether the root is present in the root history
*/ */
function isKnownRoot(uint256 _root) public view returns(bool) { function isKnownRoot(bytes32 _root) public view returns(bool) {
if (_root == 0) { if (_root == 0) {
return false; return false;
} }
// search most recent first uint32 i = currentRootIndex;
uint256 i; do {
for(i = currentRootIndex; i < 2**256 - 1; i--) { if (_root == roots[i]) {
if (_root == roots[i]) { return true;
return true; }
} if (i == 0) {
} i = ROOT_HISTORY_SIZE;
}
// process the rest of roots i--;
for(i = ROOT_HISTORY_SIZE - 1; i > currentRootIndex; i--) { } while (i != currentRootIndex);
if (_root == roots[i]) {
return true;
}
}
return false; return false;
} }
/** /**
@dev Returns the last root @dev Returns the last root
*/ */
function getLastRoot() public view returns(uint256) { function getLastRoot() public view returns(bytes32) {
return roots[currentRootIndex]; return roots[currentRootIndex];
} }
} }

View file

@ -14,14 +14,14 @@ pragma solidity ^0.5.8;
import "./MerkleTreeWithHistory.sol"; import "./MerkleTreeWithHistory.sol";
contract IVerifier { contract IVerifier {
function verifyProof(uint256[8] memory _proof, uint256[6] memory _input) public returns(bool); function verifyProof(bytes memory _proof, uint256[6] memory _input) public returns(bool);
} }
contract Mixer is MerkleTreeWithHistory { contract Mixer is MerkleTreeWithHistory {
uint256 public denomination; uint256 public denomination;
mapping(uint256 => bool) public nullifierHashes; mapping(bytes32 => bool) public nullifierHashes;
// we store all commitments just to prevent accidental deposits with the same commitment // we store all commitments just to prevent accidental deposits with the same commitment
mapping(uint256 => bool) public commitments; mapping(bytes32 => bool) public commitments;
IVerifier public verifier; IVerifier public verifier;
// operator can // operator can
@ -35,8 +35,8 @@ contract Mixer is MerkleTreeWithHistory {
_; _;
} }
event Deposit(uint256 indexed commitment, uint256 leafIndex, uint256 timestamp); event Deposit(bytes32 indexed commitment, uint32 leafIndex, uint256 timestamp);
event Withdrawal(address to, uint256 nullifierHash, address indexed relayer, uint256 fee); event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 fee);
/** /**
@dev The constructor @dev The constructor
@ -48,7 +48,7 @@ contract Mixer is MerkleTreeWithHistory {
constructor( constructor(
IVerifier _verifier, IVerifier _verifier,
uint256 _denomination, uint256 _denomination,
uint8 _merkleTreeHeight, uint32 _merkleTreeHeight,
address _operator address _operator
) MerkleTreeWithHistory(_merkleTreeHeight) public { ) MerkleTreeWithHistory(_merkleTreeHeight) public {
require(_denomination > 0, "denomination should be greater than 0"); require(_denomination > 0, "denomination should be greater than 0");
@ -61,10 +61,11 @@ contract Mixer is MerkleTreeWithHistory {
@dev Deposit funds into mixer. The caller must send (for ETH) or approve (for ERC20) value equal to or `denomination` of this mixer. @dev Deposit funds into mixer. The caller must send (for ETH) or approve (for ERC20) value equal to or `denomination` of this mixer.
@param _commitment the note commitment, which is PedersenHash(nullifier + secret) @param _commitment the note commitment, which is PedersenHash(nullifier + secret)
*/ */
function deposit(uint256 _commitment) public payable { function deposit(bytes32 _commitment) external payable {
require(!isDepositsDisabled, "deposits are disabled"); require(!isDepositsDisabled, "deposits are disabled");
require(!commitments[_commitment], "The commitment has been submitted"); require(!commitments[_commitment], "The commitment has been submitted");
uint256 insertedIndex = _insert(_commitment);
uint32 insertedIndex = _insert(_commitment);
commitments[_commitment] = true; commitments[_commitment] = true;
_processDeposit(); _processDeposit();
@ -79,31 +80,25 @@ contract Mixer is MerkleTreeWithHistory {
`input` array consists of: `input` array consists of:
- merkle root of all deposits in the mixer - merkle root of all deposits in the mixer
- hash of unique deposit nullifier to prevent double spends - hash of unique deposit nullifier to prevent double spends
- the receiver of funds - the recipient of funds
- optional fee that goes to the transaction sender (usually a relay) - optional fee that goes to the transaction sender (usually a relay)
*/ */
function withdraw(uint256[8] memory _proof, uint256[6] memory _input) public payable { function withdraw(bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) external payable {
uint256 root = _input[0]; require(_fee <= denomination, "Fee exceeds transfer value");
uint256 nullifierHash = _input[1]; require(!nullifierHashes[_nullifierHash], "The note has been already spent");
address payable receiver = address(_input[2]); require(isKnownRoot(_root), "Cannot find your merkle root"); // Make sure to use a recent one
address payable relayer = address(_input[3]); require(verifier.verifyProof(_proof, [uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund]), "Invalid withdraw proof");
uint256 fee = _input[4];
uint256 refund = _input[5];
require(fee <= denomination, "Fee exceeds transfer value");
require(!nullifierHashes[nullifierHash], "The note has been already spent");
require(isKnownRoot(root), "Cannot find your merkle root"); // Make sure to use a recent one nullifierHashes[_nullifierHash] = true;
require(verifier.verifyProof(_proof, _input), "Invalid withdraw proof"); _processWithdraw(_recipient, _relayer, _fee, _refund);
nullifierHashes[nullifierHash] = true; emit Withdrawal(_recipient, _nullifierHash, _relayer, _fee);
_processWithdraw(receiver, relayer, fee, refund);
emit Withdrawal(receiver, nullifierHash, relayer, fee);
} }
/** @dev this function is defined in a child contract */ /** @dev this function is defined in a child contract */
function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee, uint256 _refund) internal; function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) internal;
/** @dev whether a note is already spent */ /** @dev whether a note is already spent */
function isSpent(uint256 _nullifierHash) public view returns(bool) { function isSpent(bytes32 _nullifierHash) external view returns(bool) {
return nullifierHashes[_nullifierHash]; return nullifierHashes[_nullifierHash];
} }

View file

@ -4,9 +4,9 @@ import '../MerkleTreeWithHistory.sol';
contract MerkleTreeWithHistoryMock is MerkleTreeWithHistory { contract MerkleTreeWithHistoryMock is MerkleTreeWithHistory {
constructor (uint8 _treeLevels) MerkleTreeWithHistory(_treeLevels) public {} constructor (uint32 _treeLevels) MerkleTreeWithHistory(_treeLevels) public {}
function insert(uint256 _leaf) public { function insert(bytes32 _leaf) public {
_insert(_leaf); _insert(_leaf);
} }
} }

View file

@ -17,6 +17,10 @@ const takeSnapshot = async () => {
return await send('evm_snapshot') return await send('evm_snapshot')
} }
const traceTransaction = async (tx) => {
return await send('debug_traceTransaction', [tx, {}])
}
const revertSnapshot = async (id) => { const revertSnapshot = async (id) => {
await send('evm_revert', [id]) await send('evm_revert', [id])
} }
@ -44,4 +48,5 @@ module.exports = {
minerStop, minerStop,
minerStart, minerStart,
increaseTime, increaseTime,
traceTransaction
} }

View file

@ -2,7 +2,7 @@
const path = require('path') const path = require('path')
const genContract = require('circomlib/src/mimcsponge_gencontract.js') const genContract = require('circomlib/src/mimcsponge_gencontract.js')
const Artifactor = require('truffle-artifactor') const Artifactor = require('@truffle/artifactor')
module.exports = function(deployer) { module.exports = function(deployer) {
return deployer.then( async () => { return deployer.then( async () => {

9808
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@
"migrate": "npm run migrate:kovan", "migrate": "npm run migrate:kovan",
"migrate:dev": "npx truffle migrate --network development --reset", "migrate:dev": "npx truffle migrate --network development --reset",
"migrate:kovan": "npx truffle migrate --network kovan --reset", "migrate:kovan": "npx truffle migrate --network kovan --reset",
"migrate:rinkeby": "npx truffle migrate --network rinkeby --reset",
"migrate:mainnet": "npx truffle migrate --network mainnet", "migrate:mainnet": "npx truffle migrate --network mainnet",
"eslint": "npx eslint --ignore-path .gitignore .", "eslint": "npx eslint --ignore-path .gitignore .",
"flat": "truffle-flattener contracts/ETHMixer.sol > ETHMixer_flat.sol contracts/ERC20Mixer.sol > ERC20Mixer_flat.sol" "flat": "truffle-flattener contracts/ETHMixer.sol > ETHMixer_flat.sol contracts/ERC20Mixer.sol > ERC20Mixer_flat.sol"
@ -25,26 +26,24 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@openzeppelin/contracts": "^2.3.0", "@openzeppelin/contracts": "^2.4.0",
"@truffle/artifactor": "^4.0.38",
"@truffle/contract": "^4.0.39",
"@truffle/hdwallet-provider": "^1.0.24",
"bn-chai": "^1.0.1", "bn-chai": "^1.0.1",
"browserify": "^16.3.0", "browserify": "^16.5.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"circom": "0.0.34", "circom": "0.0.34",
"circomlib": "^0.0.18", "circomlib": "^0.0.19",
"dotenv": "^8.0.0", "dotenv": "^8.2.0",
"eslint": "^6.2.2", "eslint": "^6.6.0",
"ganache-cli": "^6.4.5", "ganache-cli": "^6.7.0",
"snarkjs": "git+https://github.com/peppersec/snarkjs.git#0e2f8ab28092ee6d922dc4d3ac7afc8ef5a25154", "snarkjs": "git+https://github.com/peppersec/snarkjs.git#869181cfaf7526fe8972073d31655493a04326d5",
"truffle": "^5.0.27", "truffle": "^5.0.44",
"truffle-artifactor": "^4.0.23", "truffle-flattener": "^1.4.2",
"truffle-contract": "^4.0.24", "web3": "^1.2.2",
"truffle-hdwallet-provider": "^1.0.14", "web3-utils": "^1.2.2",
"web3": "^1.0.0-beta.55", "websnark": "git+https://github.com/peppersec/websnark.git#c254b5962287b788081be1047fa0041c2885b39f"
"web3-utils": "^1.0.0-beta.55",
"websnark": "git+https://github.com/peppersec/websnark.git#966eafc47df639195c98374d3c366c32acd6f231"
},
"devDependencies": {
"truffle-flattener": "^1.4.0"
} }
} }

View file

@ -35,12 +35,19 @@ function generateDeposit() {
return deposit return deposit
} }
function getRandomReceiver() { function getRandomRecipient() {
let receiver = rbigint(20) let recipient = rbigint(20)
while (toHex(receiver.toString()).length !== 42) { while (toHex(recipient.toString()).length !== 42) {
receiver = rbigint(20) recipient = rbigint(20)
} }
return receiver return recipient
}
function toFixedHex(number, length = 32) {
let str = bigInt(number).toString(16)
while (str.length < length * 2) str = '0' + str
str = '0x' + str
return str
} }
contract('ERC20Mixer', accounts => { contract('ERC20Mixer', accounts => {
@ -56,7 +63,7 @@ contract('ERC20Mixer', accounts => {
let tree let tree
const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17) const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
const refund = ETH_AMOUNT || '1000000000000000000' // 1 ether const refund = ETH_AMOUNT || '1000000000000000000' // 1 ether
const receiver = getRandomReceiver() const recipient = getRandomRecipient()
const relayer = accounts[1] const relayer = accounts[1]
let groth16 let groth16
let circuit let circuit
@ -91,18 +98,18 @@ contract('ERC20Mixer', accounts => {
describe('#deposit', () => { describe('#deposit', () => {
it('should work', async () => { it('should work', async () => {
const commitment = 43 const commitment = toFixedHex(43)
await token.approve(mixer.address, tokenDenomination) await token.approve(mixer.address, tokenDenomination)
let { logs } = await mixer.deposit(commitment, { from: sender }) let { logs } = await mixer.deposit(commitment, { from: sender })
logs[0].event.should.be.equal('Deposit') logs[0].event.should.be.equal('Deposit')
logs[0].args.commitment.should.be.eq.BN(toBN(commitment)) logs[0].args.commitment.should.be.equal(commitment)
logs[0].args.leafIndex.should.be.eq.BN(toBN(0)) logs[0].args.leafIndex.should.be.eq.BN(0)
}) })
it('should not allow to send ether on deposit', async () => { it('should not allow to send ether on deposit', async () => {
const commitment = 43 const commitment = toFixedHex(43)
await token.approve(mixer.address, tokenDenomination) await token.approve(mixer.address, tokenDenomination)
let error = await mixer.deposit(commitment, { from: sender, value: 1e6 }).should.be.rejected let error = await mixer.deposit(commitment, { from: sender, value: 1e6 }).should.be.rejected
@ -122,7 +129,7 @@ contract('ERC20Mixer', accounts => {
// Uncomment to measure gas usage // Uncomment to measure gas usage
// let gas = await mixer.deposit.estimateGas(toBN(deposit.commitment.toString()), { from: user, gasPrice: '0' }) // let gas = await mixer.deposit.estimateGas(toBN(deposit.commitment.toString()), { from: user, gasPrice: '0' })
// console.log('deposit gas:', gas) // console.log('deposit gas:', gas)
await mixer.deposit(toBN(deposit.commitment.toString()), { from: user, gasPrice: '0' }) await mixer.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
const balanceUserAfter = await token.balanceOf(user) const balanceUserAfter = await token.balanceOf(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination))) balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination)))
@ -134,7 +141,7 @@ contract('ERC20Mixer', accounts => {
root, root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer, relayer,
receiver, recipient,
fee, fee,
refund, refund,
@ -147,27 +154,35 @@ contract('ERC20Mixer', accounts => {
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) const { proof } = websnarkUtils.toSolidityInput(proofData)
const balanceMixerBefore = await token.balanceOf(mixer.address) const balanceMixerBefore = await token.balanceOf(mixer.address)
const balanceRelayerBefore = await token.balanceOf(relayer) const balanceRelayerBefore = await token.balanceOf(relayer)
const balanceRecieverBefore = await token.balanceOf(toHex(receiver.toString())) const balanceRecieverBefore = await token.balanceOf(toHex(recipient.toString()))
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator) const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
const ethBalanceRecieverBefore = await web3.eth.getBalance(toHex(receiver.toString())) const ethBalanceRecieverBefore = await web3.eth.getBalance(toHex(recipient.toString()))
const ethBalanceRelayerBefore = await web3.eth.getBalance(relayer) const ethBalanceRelayerBefore = await web3.eth.getBalance(relayer)
let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) let isSpent = await mixer.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(false) isSpent.should.be.equal(false)
// Uncomment to measure gas usage // Uncomment to measure gas usage
// gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' }) // gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
// console.log('withdraw gas:', gas) // console.log('withdraw gas:', gas)
const { logs } = await mixer.withdraw(proof, publicSignals, { value: refund, from: relayer, gasPrice: '0' }) const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
const { logs } = await mixer.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
const balanceMixerAfter = await token.balanceOf(mixer.address) const balanceMixerAfter = await token.balanceOf(mixer.address)
const balanceRelayerAfter = await token.balanceOf(relayer) const balanceRelayerAfter = await token.balanceOf(relayer)
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator) const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
const balanceRecieverAfter = await token.balanceOf(toHex(receiver.toString())) const balanceRecieverAfter = await token.balanceOf(toHex(recipient.toString()))
const ethBalanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString())) const ethBalanceRecieverAfter = await web3.eth.getBalance(toHex(recipient.toString()))
const ethBalanceRelayerAfter = await web3.eth.getBalance(relayer) const ethBalanceRelayerAfter = await web3.eth.getBalance(relayer)
const feeBN = toBN(fee.toString()) const feeBN = toBN(fee.toString())
balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination))) balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination)))
@ -179,10 +194,10 @@ contract('ERC20Mixer', accounts => {
ethBalanceRelayerAfter.should.be.eq.BN(toBN(ethBalanceRelayerBefore).sub(toBN(refund))) ethBalanceRelayerAfter.should.be.eq.BN(toBN(ethBalanceRelayerBefore).sub(toBN(refund)))
logs[0].event.should.be.equal('Withdrawal') logs[0].event.should.be.equal('Withdrawal')
logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString())) logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash))
logs[0].args.relayer.should.be.eq.BN(relayer) logs[0].args.relayer.should.be.eq.BN(relayer)
logs[0].args.fee.should.be.eq.BN(feeBN) logs[0].args.fee.should.be.eq.BN(feeBN)
isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) isSpent = await mixer.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(true) isSpent.should.be.equal(true)
}) })
@ -192,7 +207,7 @@ contract('ERC20Mixer', accounts => {
await tree.insert(deposit.commitment) await tree.insert(deposit.commitment)
await token.mint(user, tokenDenomination) await token.mint(user, tokenDenomination)
await token.approve(mixer.address, tokenDenomination, { from: user }) await token.approve(mixer.address, tokenDenomination, { from: user })
await mixer.deposit(toBN(deposit.commitment.toString()), { from: user, gasPrice: '0' }) await mixer.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
const { root, path_elements, path_index } = await tree.path(0) const { root, path_elements, path_index } = await tree.path(0)
@ -202,7 +217,7 @@ contract('ERC20Mixer', accounts => {
root, root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer, relayer,
receiver, recipient,
fee, fee,
refund, refund,
@ -215,13 +230,21 @@ contract('ERC20Mixer', accounts => {
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) const { proof } = websnarkUtils.toSolidityInput(proofData)
let { reason } = await mixer.withdraw(proof, publicSignals, { value: 1, from: relayer, gasPrice: '0' }).should.be.rejected const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
let { reason } = await mixer.withdraw(proof, ...args, { value: 1, from: relayer, gasPrice: '0' }).should.be.rejected
reason.should.be.equal('Incorrect refund amount received by the contract') reason.should.be.equal('Incorrect refund amount received by the contract')
;({ reason } = await mixer.withdraw(proof, publicSignals, { value: toBN(refund).mul(toBN(2)), from: relayer, gasPrice: '0' }).should.be.rejected) ;({ reason } = await mixer.withdraw(proof, ...args, { value: toBN(refund).mul(toBN(2)), from: relayer, gasPrice: '0' }).should.be.rejected)
reason.should.be.equal('Incorrect refund amount received by the contract') reason.should.be.equal('Incorrect refund amount received by the contract')
}) })
@ -247,7 +270,7 @@ contract('ERC20Mixer', accounts => {
console.log('approve done') console.log('approve done')
const allowanceUser = await usdtToken.allowance(user, mixer.address) const allowanceUser = await usdtToken.allowance(user, mixer.address)
console.log('allowanceUser', allowanceUser.toString()) console.log('allowanceUser', allowanceUser.toString())
await mixer.deposit(toBN(deposit.commitment.toString()), { from: user, gasPrice: '0' }) await mixer.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
console.log('deposit done') console.log('deposit done')
const balanceUserAfter = await usdtToken.balanceOf(user) const balanceUserAfter = await usdtToken.balanceOf(user)
@ -261,7 +284,7 @@ contract('ERC20Mixer', accounts => {
root, root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer: operator, relayer: operator,
receiver, recipient,
fee, fee,
refund, refund,
@ -274,26 +297,34 @@ contract('ERC20Mixer', accounts => {
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) const { proof } = websnarkUtils.toSolidityInput(proofData)
const balanceMixerBefore = await usdtToken.balanceOf(mixer.address) const balanceMixerBefore = await usdtToken.balanceOf(mixer.address)
const balanceRelayerBefore = await usdtToken.balanceOf(relayer) const balanceRelayerBefore = await usdtToken.balanceOf(relayer)
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator) const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
const balanceRecieverBefore = await usdtToken.balanceOf(toHex(receiver.toString())) const balanceRecieverBefore = await usdtToken.balanceOf(toHex(recipient.toString()))
const ethBalanceRecieverBefore = await web3.eth.getBalance(toHex(receiver.toString())) const ethBalanceRecieverBefore = await web3.eth.getBalance(toHex(recipient.toString()))
let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
isSpent.should.be.equal(false) isSpent.should.be.equal(false)
// Uncomment to measure gas usage // Uncomment to measure gas usage
// gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' }) // gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
// console.log('withdraw gas:', gas) // console.log('withdraw gas:', gas)
const { logs } = await mixer.withdraw(proof, publicSignals, { value: refund, from: relayer, gasPrice: '0' }) const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
const { logs } = await mixer.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
const balanceMixerAfter = await usdtToken.balanceOf(mixer.address) const balanceMixerAfter = await usdtToken.balanceOf(mixer.address)
const balanceRelayerAfter = await usdtToken.balanceOf(relayer) const balanceRelayerAfter = await usdtToken.balanceOf(relayer)
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator) const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
const balanceRecieverAfter = await usdtToken.balanceOf(toHex(receiver.toString())) const balanceRecieverAfter = await usdtToken.balanceOf(toHex(recipient.toString()))
const ethBalanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString())) const ethBalanceRecieverAfter = await web3.eth.getBalance(toHex(recipient.toString()))
const feeBN = toBN(fee.toString()) const feeBN = toBN(fee.toString())
balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination))) balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination)))
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore)) balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
@ -328,7 +359,7 @@ contract('ERC20Mixer', accounts => {
console.log('balanceUserBefore', balanceUserBefore.toString()) console.log('balanceUserBefore', balanceUserBefore.toString())
await token.approve(mixer.address, tokenDenomination, { from: user }) await token.approve(mixer.address, tokenDenomination, { from: user })
console.log('approve done') console.log('approve done')
await mixer.deposit(toBN(deposit.commitment.toString()), { from: user, gasPrice: '0' }) await mixer.deposit(toFixedHex(deposit.commitment), { from: user, gasPrice: '0' })
console.log('deposit done') console.log('deposit done')
const balanceUserAfter = await token.balanceOf(user) const balanceUserAfter = await token.balanceOf(user)
@ -342,7 +373,7 @@ contract('ERC20Mixer', accounts => {
root, root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer: operator, relayer: operator,
receiver, recipient,
fee, fee,
refund, refund,
@ -355,27 +386,35 @@ contract('ERC20Mixer', accounts => {
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) const { proof } = websnarkUtils.toSolidityInput(proofData)
const balanceMixerBefore = await token.balanceOf(mixer.address) const balanceMixerBefore = await token.balanceOf(mixer.address)
const balanceRelayerBefore = await token.balanceOf(relayer) const balanceRelayerBefore = await token.balanceOf(relayer)
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator) const ethBalanceOperatorBefore = await web3.eth.getBalance(operator)
const balanceRecieverBefore = await token.balanceOf(toHex(receiver.toString())) const balanceRecieverBefore = await token.balanceOf(toHex(recipient.toString()))
const ethBalanceRecieverBefore = await web3.eth.getBalance(toHex(receiver.toString())) const ethBalanceRecieverBefore = await web3.eth.getBalance(toHex(recipient.toString()))
let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
isSpent.should.be.equal(false) isSpent.should.be.equal(false)
// Uncomment to measure gas usage // Uncomment to measure gas usage
// gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' }) // gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
// console.log('withdraw gas:', gas) // console.log('withdraw gas:', gas)
const { logs } = await mixer.withdraw(proof, publicSignals, { value: refund, from: relayer, gasPrice: '0' }) const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
const { logs } = await mixer.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' })
console.log('withdraw done') console.log('withdraw done')
const balanceMixerAfter = await token.balanceOf(mixer.address) const balanceMixerAfter = await token.balanceOf(mixer.address)
const balanceRelayerAfter = await token.balanceOf(relayer) const balanceRelayerAfter = await token.balanceOf(relayer)
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator) const ethBalanceOperatorAfter = await web3.eth.getBalance(operator)
const balanceRecieverAfter = await token.balanceOf(toHex(receiver.toString())) const balanceRecieverAfter = await token.balanceOf(toHex(recipient.toString()))
const ethBalanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString())) const ethBalanceRecieverAfter = await web3.eth.getBalance(toHex(recipient.toString()))
const feeBN = toBN(fee.toString()) const feeBN = toBN(fee.toString())
balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination))) balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination)))
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore)) balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))

View file

@ -43,12 +43,12 @@ function BNArrayToStringArray(array) {
return arrayToPrint return arrayToPrint
} }
function getRandomReceiver() { function getRandomRecipient() {
let receiver = rbigint(20) let recipient = rbigint(20)
while (toHex(receiver.toString()).length !== 42) { while (toHex(recipient.toString()).length !== 42) {
receiver = rbigint(20) recipient = rbigint(20)
} }
return receiver return recipient
} }
function snarkVerify(proof) { function snarkVerify(proof) {
@ -57,6 +57,13 @@ function snarkVerify(proof) {
return snarkjs['groth'].isValid(verification_key, proof, proof.publicSignals) return snarkjs['groth'].isValid(verification_key, proof, proof.publicSignals)
} }
function toFixedHex(number, length = 32) {
let str = bigInt(number).toString(16)
while (str.length < length * 2) str = '0' + str
str = '0x' + str
return str
}
contract('ETHMixer', accounts => { contract('ETHMixer', accounts => {
let mixer let mixer
const sender = accounts[0] const sender = accounts[0]
@ -68,7 +75,7 @@ contract('ETHMixer', accounts => {
let tree let tree
const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17) const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
const refund = bigInt(0) const refund = bigInt(0)
const receiver = getRandomReceiver() const recipient = getRandomRecipient()
const relayer = accounts[1] const relayer = accounts[1]
let groth16 let groth16
let circuit let circuit
@ -96,23 +103,23 @@ contract('ETHMixer', accounts => {
describe('#deposit', () => { describe('#deposit', () => {
it('should emit event', async () => { it('should emit event', async () => {
let commitment = 42 let commitment = toFixedHex(42)
let { logs } = await mixer.deposit(commitment, { value, from: sender }) let { logs } = await mixer.deposit(commitment, { value, from: sender })
logs[0].event.should.be.equal('Deposit') logs[0].event.should.be.equal('Deposit')
logs[0].args.commitment.should.be.eq.BN(toBN(commitment)) logs[0].args.commitment.should.be.equal(commitment)
logs[0].args.leafIndex.should.be.eq.BN(toBN(0)) logs[0].args.leafIndex.should.be.eq.BN(0)
commitment = 12; commitment = toFixedHex(12);
({ logs } = await mixer.deposit(commitment, { value, from: accounts[2] })) ({ logs } = await mixer.deposit(commitment, { value, from: accounts[2] }))
logs[0].event.should.be.equal('Deposit') logs[0].event.should.be.equal('Deposit')
logs[0].args.commitment.should.be.eq.BN(toBN(commitment)) logs[0].args.commitment.should.be.equal(commitment)
logs[0].args.leafIndex.should.be.eq.BN(toBN(1)) logs[0].args.leafIndex.should.be.eq.BN(1)
}) })
it('should not deposit if disabled', async () => { it('should not deposit if disabled', async () => {
let commitment = 42; let commitment = toFixedHex(42);
(await mixer.isDepositsDisabled()).should.be.equal(false) (await mixer.isDepositsDisabled()).should.be.equal(false)
const err = await mixer.toggleDeposits(true, { from: accounts[1] }).should.be.rejected const err = await mixer.toggleDeposits(true, { from: accounts[1] }).should.be.rejected
err.reason.should.be.equal('Only operator can call this function.') err.reason.should.be.equal('Only operator can call this function.')
@ -127,7 +134,7 @@ contract('ETHMixer', accounts => {
}) })
it('should throw if there is a such commitment', async () => { it('should throw if there is a such commitment', async () => {
const commitment = 42 const commitment = toFixedHex(42)
await mixer.deposit(commitment, { value, from: sender }).should.be.fulfilled await mixer.deposit(commitment, { value, from: sender }).should.be.fulfilled
const error = await mixer.deposit(commitment, { value, from: sender }).should.be.rejected const error = await mixer.deposit(commitment, { value, from: sender }).should.be.rejected
error.reason.should.be.equal('The commitment has been submitted') error.reason.should.be.equal('The commitment has been submitted')
@ -145,7 +152,7 @@ contract('ETHMixer', accounts => {
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier, nullifier: deposit.nullifier,
relayer: operator, relayer: operator,
receiver, recipient,
fee, fee,
refund, refund,
secret: deposit.secret, secret: deposit.secret,
@ -189,7 +196,7 @@ contract('ETHMixer', accounts => {
// Uncomment to measure gas usage // Uncomment to measure gas usage
// let gas = await mixer.deposit.estimateGas(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' }) // let gas = await mixer.deposit.estimateGas(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
// console.log('deposit gas:', gas) // console.log('deposit gas:', gas)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' }) await mixer.deposit(toFixedHex(deposit.commitment), { value, from: user, gasPrice: '0' })
const balanceUserAfter = await web3.eth.getBalance(user) const balanceUserAfter = await web3.eth.getBalance(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(value))) balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(value)))
@ -202,7 +209,7 @@ contract('ETHMixer', accounts => {
root, root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer: operator, relayer: operator,
receiver, recipient,
fee, fee,
refund, refund,
@ -215,24 +222,32 @@ contract('ETHMixer', accounts => {
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) const { proof } = websnarkUtils.toSolidityInput(proofData)
const balanceMixerBefore = await web3.eth.getBalance(mixer.address) const balanceMixerBefore = await web3.eth.getBalance(mixer.address)
const balanceRelayerBefore = await web3.eth.getBalance(relayer) const balanceRelayerBefore = await web3.eth.getBalance(relayer)
const balanceOperatorBefore = await web3.eth.getBalance(operator) const balanceOperatorBefore = await web3.eth.getBalance(operator)
const balanceRecieverBefore = await web3.eth.getBalance(toHex(receiver.toString())) const balanceRecieverBefore = await web3.eth.getBalance(toHex(recipient.toString()))
let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) let isSpent = await mixer.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(false) isSpent.should.be.equal(false)
// Uncomment to measure gas usage // Uncomment to measure gas usage
// gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' }) // gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
// console.log('withdraw gas:', gas) // console.log('withdraw gas:', gas)
const { logs } = await mixer.withdraw(proof, publicSignals, { from: relayer, gasPrice: '0' }) const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
const { logs } = await mixer.withdraw(proof, ...args, { from: relayer, gasPrice: '0' })
const balanceMixerAfter = await web3.eth.getBalance(mixer.address) const balanceMixerAfter = await web3.eth.getBalance(mixer.address)
const balanceRelayerAfter = await web3.eth.getBalance(relayer) const balanceRelayerAfter = await web3.eth.getBalance(relayer)
const balanceOperatorAfter = await web3.eth.getBalance(operator) const balanceOperatorAfter = await web3.eth.getBalance(operator)
const balanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString())) const balanceRecieverAfter = await web3.eth.getBalance(toHex(recipient.toString()))
const feeBN = toBN(fee.toString()) const feeBN = toBN(fee.toString())
balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(value))) balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(value)))
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore)) balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
@ -241,17 +256,17 @@ contract('ETHMixer', accounts => {
logs[0].event.should.be.equal('Withdrawal') logs[0].event.should.be.equal('Withdrawal')
logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString())) logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash))
logs[0].args.relayer.should.be.eq.BN(operator) logs[0].args.relayer.should.be.eq.BN(operator)
logs[0].args.fee.should.be.eq.BN(feeBN) logs[0].args.fee.should.be.eq.BN(feeBN)
isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) isSpent = await mixer.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(true) isSpent.should.be.equal(true)
}) })
it('should prevent double spend', async () => { it('should prevent double spend', async () => {
const deposit = generateDeposit() const deposit = generateDeposit()
await tree.insert(deposit.commitment) await tree.insert(deposit.commitment)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender }) await mixer.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0) const { root, path_elements, path_index } = await tree.path(0)
@ -260,7 +275,7 @@ contract('ETHMixer', accounts => {
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier, nullifier: deposit.nullifier,
relayer: operator, relayer: operator,
receiver, recipient,
fee, fee,
refund, refund,
secret: deposit.secret, secret: deposit.secret,
@ -268,16 +283,24 @@ contract('ETHMixer', accounts => {
pathIndices: path_index, pathIndices: path_index,
}) })
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) const { proof } = websnarkUtils.toSolidityInput(proofData)
await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.fulfilled const args = [
const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
await mixer.withdraw(proof, ...args, { from: relayer }).should.be.fulfilled
const error = await mixer.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('The note has been already spent') error.reason.should.be.equal('The note has been already spent')
}) })
it('should prevent double spend with overflow', async () => { it('should prevent double spend with overflow', async () => {
const deposit = generateDeposit() const deposit = generateDeposit()
await tree.insert(deposit.commitment) await tree.insert(deposit.commitment)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender }) await mixer.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0) const { root, path_elements, path_index } = await tree.path(0)
@ -286,7 +309,7 @@ contract('ETHMixer', accounts => {
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier, nullifier: deposit.nullifier,
relayer: operator, relayer: operator,
receiver, recipient,
fee, fee,
refund, refund,
secret: deposit.secret, secret: deposit.secret,
@ -294,16 +317,23 @@ contract('ETHMixer', accounts => {
pathIndices: path_index, pathIndices: path_index,
}) })
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) const { proof } = websnarkUtils.toSolidityInput(proofData)
publicSignals[1] ='0x' + toBN(publicSignals[1]).add(toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617')).toString('hex') const args = [
const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected toFixedHex(input.root),
toFixedHex(toBN(input.nullifierHash).add(toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617'))),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
const error = await mixer.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('verifier-gte-snark-scalar-field') error.reason.should.be.equal('verifier-gte-snark-scalar-field')
}) })
it('fee should be less or equal transfer value', async () => { it('fee should be less or equal transfer value', async () => {
const deposit = generateDeposit() const deposit = generateDeposit()
await tree.insert(deposit.commitment) await tree.insert(deposit.commitment)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender }) await mixer.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0) const { root, path_elements, path_index } = await tree.path(0)
const oneEtherFee = bigInt(1e18) // 1 ether const oneEtherFee = bigInt(1e18) // 1 ether
@ -312,7 +342,7 @@ contract('ETHMixer', accounts => {
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier, nullifier: deposit.nullifier,
relayer: operator, relayer: operator,
receiver, recipient,
fee: oneEtherFee, fee: oneEtherFee,
refund, refund,
secret: deposit.secret, secret: deposit.secret,
@ -321,15 +351,23 @@ contract('ETHMixer', accounts => {
}) })
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) const { proof } = websnarkUtils.toSolidityInput(proofData)
const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
const error = await mixer.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Fee exceeds transfer value') error.reason.should.be.equal('Fee exceeds transfer value')
}) })
it('should throw for corrupted merkle tree root', async () => { it('should throw for corrupted merkle tree root', async () => {
const deposit = generateDeposit() const deposit = generateDeposit()
await tree.insert(deposit.commitment) await tree.insert(deposit.commitment)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender }) await mixer.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0) const { root, path_elements, path_index } = await tree.path(0)
@ -338,7 +376,7 @@ contract('ETHMixer', accounts => {
root, root,
nullifier: deposit.nullifier, nullifier: deposit.nullifier,
relayer: operator, relayer: operator,
receiver, recipient,
fee, fee,
refund, refund,
secret: deposit.secret, secret: deposit.secret,
@ -346,19 +384,25 @@ contract('ETHMixer', accounts => {
pathIndices: path_index, pathIndices: path_index,
}) })
const dummyRoot = randomHex(32)
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) const { proof } = websnarkUtils.toSolidityInput(proofData)
publicSignals[0] = dummyRoot
const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected const args = [
toFixedHex(randomHex(32)),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
const error = await mixer.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Cannot find your merkle root') error.reason.should.be.equal('Cannot find your merkle root')
}) })
it('should reject with tampered public inputs', async () => { it('should reject with tampered public inputs', async () => {
const deposit = generateDeposit() const deposit = generateDeposit()
await tree.insert(deposit.commitment) await tree.insert(deposit.commitment)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender }) await mixer.deposit(toFixedHex(deposit.commitment), { value, from: sender })
let { root, path_elements, path_index } = await tree.path(0) let { root, path_elements, path_index } = await tree.path(0)
@ -367,7 +411,7 @@ contract('ETHMixer', accounts => {
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier, nullifier: deposit.nullifier,
relayer: operator, relayer: operator,
receiver, recipient,
fee, fee,
refund, refund,
secret: deposit.secret, secret: deposit.secret,
@ -375,42 +419,66 @@ contract('ETHMixer', accounts => {
pathIndices: path_index, pathIndices: path_index,
}) })
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
let { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) let { proof } = websnarkUtils.toSolidityInput(proofData)
const originalPublicSignals = publicSignals.slice() const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
let incorrectArgs
const originalProof = proof.slice() const originalProof = proof.slice()
// receiver // recipient
publicSignals[2] = '0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337' incorrectArgs = [
toFixedHex(input.root),
let error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected toFixedHex(input.nullifierHash),
toFixedHex('0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337', 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
let error = await mixer.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof') error.reason.should.be.equal('Invalid withdraw proof')
// fee // fee
publicSignals = originalPublicSignals.slice() incorrectArgs = [
publicSignals[3] = '0x000000000000000000000000000000000000000000000000015345785d8a0000' toFixedHex(input.root),
toFixedHex(input.nullifierHash),
error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex('0x000000000000000000000000000000000000000000000000015345785d8a0000'),
toFixedHex(input.refund)
]
error = await mixer.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof') error.reason.should.be.equal('Invalid withdraw proof')
// nullifier // nullifier
publicSignals = originalPublicSignals.slice() incorrectArgs = [
publicSignals[1] = '0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad' toFixedHex(input.root),
toFixedHex('0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad'),
error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
error = await mixer.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof') error.reason.should.be.equal('Invalid withdraw proof')
// proof itself // proof itself
proof[0] = '0x261d81d8203437f29b38a88c4263476d858e6d9645cf21740461684412b31337' proof = '0xbeef' + proof.substr(6)
await mixer.withdraw(proof, originalPublicSignals, { from: relayer }).should.be.rejected await mixer.withdraw(proof, ...args, { from: relayer }).should.be.rejected
// should work with original values // should work with original values
await mixer.withdraw(originalProof, originalPublicSignals, { from: relayer }).should.be.fulfilled await mixer.withdraw(originalProof, ...args, { from: relayer }).should.be.fulfilled
}) })
it('should reject with non zero refund', async () => { it('should reject with non zero refund', async () => {
const deposit = generateDeposit() const deposit = generateDeposit()
await tree.insert(deposit.commitment) await tree.insert(deposit.commitment)
await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender }) await mixer.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0) const { root, path_elements, path_index } = await tree.path(0)
@ -419,7 +487,7 @@ contract('ETHMixer', accounts => {
root, root,
nullifier: deposit.nullifier, nullifier: deposit.nullifier,
relayer: operator, relayer: operator,
receiver, recipient,
fee, fee,
refund: bigInt(1), refund: bigInt(1),
secret: deposit.secret, secret: deposit.secret,
@ -428,9 +496,17 @@ contract('ETHMixer', accounts => {
}) })
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) const { proof } = websnarkUtils.toSolidityInput(proofData)
const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
const error = await mixer.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Refund value is supposed to be zero for ETH mixer') error.reason.should.be.equal('Refund value is supposed to be zero for ETH mixer')
}) })
}) })

View file

@ -12,6 +12,9 @@ const hasherContract = artifacts.require('./Hasher.sol')
const MerkleTree = require('../lib/MerkleTree') const MerkleTree = require('../lib/MerkleTree')
const hasherImpl = require('../lib/MiMC') const hasherImpl = require('../lib/MiMC')
const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt
const { ETH_AMOUNT, MERKLE_TREE_HEIGHT } = process.env const { ETH_AMOUNT, MERKLE_TREE_HEIGHT } = process.env
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
@ -23,6 +26,13 @@ function BNArrayToStringArray(array) {
return arrayToPrint return arrayToPrint
} }
function toFixedHex(number, length = 32) {
let str = bigInt(number).toString(16)
while (str.length < length * 2) str = '0' + str
str = '0x' + str
return str
}
contract('MerkleTreeWithHistory', accounts => { contract('MerkleTreeWithHistory', accounts => {
let merkleTreeWithHistory let merkleTreeWithHistory
let hasherInstance let hasherInstance
@ -51,9 +61,9 @@ contract('MerkleTreeWithHistory', accounts => {
it('should initialize', async () => { it('should initialize', async () => {
const zeroValue = await merkleTreeWithHistory.ZERO_VALUE() const zeroValue = await merkleTreeWithHistory.ZERO_VALUE()
const firstSubtree = await merkleTreeWithHistory.filledSubtrees(0) const firstSubtree = await merkleTreeWithHistory.filledSubtrees(0)
firstSubtree.should.be.eq.BN(zeroValue) firstSubtree.should.be.equal(toFixedHex(zeroValue))
const firstZero = await merkleTreeWithHistory.zeros(0) const firstZero = await merkleTreeWithHistory.zeros(0)
firstZero.should.be.eq.BN(zeroValue) firstZero.should.be.equal(toFixedHex(zeroValue))
}) })
}) })
@ -72,7 +82,7 @@ contract('MerkleTreeWithHistory', accounts => {
null, null,
prefix, prefix,
) )
await tree.insert('5') await tree.insert(toFixedHex('5'))
let { root, path_elements } = await tree.path(0) let { root, path_elements } = await tree.path(0)
const calculated_root = hasher.hash(null, const calculated_root = hasher.hash(null,
hasher.hash(null, '5', path_elements[0]), hasher.hash(null, '5', path_elements[0]),
@ -162,32 +172,32 @@ contract('MerkleTreeWithHistory', accounts => {
let rootFromContract let rootFromContract
for (let i = 1; i < 11; i++) { for (let i = 1; i < 11; i++) {
await merkleTreeWithHistory.insert(i, { from: sender }) await merkleTreeWithHistory.insert(toFixedHex(i), { from: sender })
await tree.insert(i) await tree.insert(i)
let { root } = await tree.path(i - 1) let { root } = await tree.path(i - 1)
rootFromContract = await merkleTreeWithHistory.getLastRoot() rootFromContract = await merkleTreeWithHistory.getLastRoot()
root.should.be.equal(rootFromContract.toString()) toFixedHex(root).should.be.equal(rootFromContract.toString())
} }
}) })
it('should reject if tree is full', async () => { it('should reject if tree is full', async () => {
levels = 6 const levels = 6
merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels) const merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels)
for (let i = 0; i < 2**levels; i++) { for (let i = 0; i < 2**levels; i++) {
await merkleTreeWithHistory.insert(i+42).should.be.fulfilled await merkleTreeWithHistory.insert(toFixedHex(i+42)).should.be.fulfilled
} }
let error = await merkleTreeWithHistory.insert(1337).should.be.rejected let error = await merkleTreeWithHistory.insert(toFixedHex(1337)).should.be.rejected
error.reason.should.be.equal('Merkle tree is full. No more leafs can be added') error.reason.should.be.equal('Merkle tree is full. No more leafs can be added')
error = await merkleTreeWithHistory.insert(1).should.be.rejected error = await merkleTreeWithHistory.insert(toFixedHex(1)).should.be.rejected
error.reason.should.be.equal('Merkle tree is full. No more leafs can be added') error.reason.should.be.equal('Merkle tree is full. No more leafs can be added')
}) })
it.skip('hasher gas', async () => { it.skip('hasher gas', async () => {
levels = 6 const levels = 6
merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels) const merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels)
const zeroValue = await merkleTreeWithHistory.zeroValue() const zeroValue = await merkleTreeWithHistory.zeroValue()
const gas = await merkleTreeWithHistory.hashLeftRight.estimateGas(zeroValue, zeroValue) const gas = await merkleTreeWithHistory.hashLeftRight.estimateGas(zeroValue, zeroValue)
@ -195,6 +205,32 @@ contract('MerkleTreeWithHistory', accounts => {
}) })
}) })
describe('#isKnownRoot', () => {
it('should work', async () => {
let path
for (let i = 1; i < 5; i++) {
await merkleTreeWithHistory.insert(toFixedHex(i), { from: sender }).should.be.fulfilled
await tree.insert(i)
path = await tree.path(i - 1)
let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(path.root))
isKnown.should.be.equal(true)
}
await merkleTreeWithHistory.insert(toFixedHex(42), { from: sender }).should.be.fulfilled
// check outdated root
let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(path.root))
isKnown.should.be.equal(true)
})
it('should not return uninitialized roots', async () => {
await merkleTreeWithHistory.insert(toFixedHex(42), { from: sender }).should.be.fulfilled
let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(0))
isKnown.should.be.equal(false)
})
})
afterEach(async () => { afterEach(async () => {
await revertSnapshot(snapshotId.result) await revertSnapshot(snapshotId.result)
// eslint-disable-next-line require-atomic-updates // eslint-disable-next-line require-atomic-updates

View file

@ -1,5 +1,5 @@
require('dotenv').config() require('dotenv').config()
const HDWalletProvider = require('truffle-hdwallet-provider') const HDWalletProvider = require('@truffle/hdwallet-provider')
const utils = require('web3-utils') const utils = require('web3-utils')
// const infuraKey = "fj4jll3k....."; // const infuraKey = "fj4jll3k.....";
// //
@ -51,6 +51,15 @@ module.exports = {
// timeoutBlocks: 200, // timeoutBlocks: 200,
skipDryRun: true skipDryRun: true
}, },
rinkeby: {
provider: () => new HDWalletProvider(process.env.PRIVATE_KEY, 'https://rinkeby.infura.io/v3/c7463beadf2144e68646ff049917b716'),
network_id: 4,
gas: 6000000,
gasPrice: utils.toWei('1', 'gwei'),
// confirmations: 0,
// timeoutBlocks: 200,
skipDryRun: true
},
mainnet: { mainnet: {
provider: () => new HDWalletProvider(process.env.PRIVATE_KEY, 'http://ethereum-rpc.trustwalletapp.com'), provider: () => new HDWalletProvider(process.env.PRIVATE_KEY, 'http://ethereum-rpc.trustwalletapp.com'),
network_id: 1, network_id: 1,