Add feeToSetter

This commit is contained in:
Brian Li 2021-04-02 15:32:58 -07:00
parent 66a8fe5712
commit 603dcdc426
19 changed files with 29460 additions and 20562 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,5 @@
}
},
"schemaVersion": "3.3.4",
"updatedAt": "2021-03-22T02:54:26.425Z",
"networkType": "ethereum"
"updatedAt": "2021-04-02T22:30:43.196Z"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -15,15 +15,19 @@ import "./Tornado.sol";
contract ERC20Tornado is Tornado {
address public token;
uint256 public protocolFee;
constructor(
IVerifier _verifier,
IFeeManager _feeManager,
uint256 _denomination,
uint32 _merkleTreeHeight,
address _operator,
address _token
) Tornado(_verifier, _denomination, _merkleTreeHeight, _operator) public {
) Tornado(_verifier, _feeManager, _denomination, _merkleTreeHeight, _operator) public {
token = _token;
// 0.5% fee
protocolFee = _denomination / 200;
}
function _processDeposit() internal {
@ -31,12 +35,19 @@ contract ERC20Tornado is Tornado {
_safeErc20TransferFrom(msg.sender, address(this), denomination);
}
function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) internal {
function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _relayer_fee, uint256 _refund, address _feeTo) internal {
require(msg.value == _refund, "Incorrect refund amount received by the contract");
_safeErc20Transfer(_recipient, denomination - _fee);
if (_fee > 0) {
_safeErc20Transfer(_relayer, _fee);
bool feeOn = _feeTo != address(0);
if (feeOn) {
_safeErc20Transfer(_recipient, denomination - _relayer_fee - protocolFee);
_safeErc20Transfer(_feeTo, protocolFee);
} else {
_safeErc20Transfer(_recipient, denomination - _relayer_fee);
}
if (_relayer_fee > 0) {
_safeErc20Transfer(_relayer, _relayer_fee);
}
if (_refund > 0) {

View File

@ -1,41 +0,0 @@
// 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.17;
import "./Tornado.sol";
contract ETHTornado is Tornado {
constructor(
IVerifier _verifier,
uint256 _denomination,
uint32 _merkleTreeHeight,
address _operator
) Tornado(_verifier, _denomination, _merkleTreeHeight, _operator) public {
}
function _processDeposit() internal {
require(msg.value == denomination, "Please send `mixDenomination` ETH along with transaction");
}
function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) internal {
// sanity checks
require(msg.value == 0, "Message value is supposed to be zero for ETH instance");
require(_refund == 0, "Refund value is supposed to be zero for ETH instance");
(bool success, ) = _recipient.call.value(denomination - _fee)("");
require(success, "payment to _recipient did not go thru");
if (_fee > 0) {
(success, ) = _relayer.call.value(_fee)("");
require(success, "payment to _relayer did not go thru");
}
}
}

20
contracts/FeeManager.sol Normal file
View File

@ -0,0 +1,20 @@
pragma solidity 0.5.17;
contract FeeManager {
address public feeTo;
address public feeToSetter;
constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}
function setFeeTo(address _feeTo) external {
require(msg.sender == feeToSetter, 'Poof: FORBIDDEN');
feeTo = _feeTo;
}
function setFeeToSetter(address _feeToSetter) external {
require(msg.sender == feeToSetter, 'Poof: FORBIDDEN');
feeToSetter = _feeToSetter;
}
}

View File

@ -1,14 +1,3 @@
// 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.17;
import "./MerkleTreeWithHistory.sol";
@ -18,12 +7,17 @@ contract IVerifier {
function verifyProof(bytes memory _proof, uint256[6] memory _input) public returns(bool);
}
contract IFeeManager {
function feeTo() external view returns (address);
}
contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
uint256 public denomination;
mapping(bytes32 => bool) public nullifierHashes;
// we store all commitments just to prevent accidental deposits with the same commitment
mapping(bytes32 => bool) public commitments;
IVerifier public verifier;
IFeeManager public feeManager;
// operator can update snark verification key
// after the final trusted setup ceremony operator rights are supposed to be transferred to zero address
@ -45,12 +39,14 @@ contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
*/
constructor(
IVerifier _verifier,
IFeeManager _feeManager,
uint256 _denomination,
uint32 _merkleTreeHeight,
address _operator
) MerkleTreeWithHistory(_merkleTreeHeight) public {
require(_denomination > 0, "denomination should be greater than 0");
verifier = _verifier;
feeManager = _feeManager;
operator = _operator;
denomination = _denomination;
}
@ -87,12 +83,12 @@ contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
require(verifier.verifyProof(_proof, [uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund]), "Invalid withdraw proof");
nullifierHashes[_nullifierHash] = true;
_processWithdraw(_recipient, _relayer, _fee, _refund);
_processWithdraw(_recipient, _relayer, _fee, _refund, feeManager.feeTo());
emit Withdrawal(_recipient, _nullifierHash, _relayer, _fee);
}
/** @dev this function is defined in a child contract */
function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) internal;
function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _relayer_fee, uint256 _refund, address _feeTo) internal;
/** @dev whether a note is already spent */
function isSpent(bytes32 _nullifierHash) public view returns(bool) {

View File

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

View File

@ -0,0 +1,6 @@
/* global artifacts */
const FeeManager = artifacts.require('FeeManager')
module.exports = function (deployer, network, accounts) {
deployer.deploy(FeeManager, accounts[0])
}

View File

@ -1,25 +1,28 @@
/* global artifacts */
require('dotenv').config({ path: '../.env' })
require('dotenv').config({path: '../.env'})
const ERC20Tornado = artifacts.require('ERC20Tornado')
const Verifier = artifacts.require('Verifier')
const FeeManager = artifacts.require('FeeManager')
const hasherContract = artifacts.require('Hasher')
const ERC20Mock = artifacts.require('ERC20Mock')
module.exports = function(deployer, network, accounts) {
module.exports = function (deployer, network, accounts) {
return deployer.then(async () => {
const { MERKLE_TREE_HEIGHT, ERC20_TOKEN, TOKEN_AMOUNT } = process.env
const {MERKLE_TREE_HEIGHT, ERC20_TOKEN, TOKEN_AMOUNT} = process.env
const verifier = await Verifier.deployed()
const feeManager = await FeeManager.deployed()
const hasherInstance = await hasherContract.deployed()
await ERC20Tornado.link(hasherContract, hasherInstance.address)
let token = ERC20_TOKEN
if(token === '') {
if (token === '') {
const tokenInstance = await deployer.deploy(ERC20Mock)
token = tokenInstance.address
}
const tornado = await deployer.deploy(
ERC20Tornado,
verifier.address,
feeManager.address,
TOKEN_AMOUNT,
MERKLE_TREE_HEIGHT,
accounts[0],

View File

@ -9,10 +9,11 @@ const { toBN } = require('web3-utils')
const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper')
const Tornado = artifacts.require('./ERC20Tornado.sol')
const FeeManager = artifacts.require('./FeeManager.sol')
const BadRecipient = artifacts.require('./BadRecipient.sol')
const Token = artifacts.require('./ERC20Mock.sol')
const USDTToken = artifacts.require('./IUSDT.sol')
const { ETH_AMOUNT, TOKEN_AMOUNT, MERKLE_TREE_HEIGHT, ERC20_TOKEN } = process.env
const { CELO_AMOUNT, TOKEN_AMOUNT, MERKLE_TREE_HEIGHT, ERC20_TOKEN } = process.env
const websnarkUtils = require('websnark/src/utils')
const buildGroth16 = require('websnark/src/groth16')
@ -40,6 +41,7 @@ function generateDeposit() {
contract('ERC20Tornado', accounts => {
let tornado
let feeManager
let token
let usdtToken
let badRecipient
@ -50,8 +52,8 @@ contract('ERC20Tornado', accounts => {
let snapshotId
let prefix = 'test'
let tree
const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
const refund = ETH_AMOUNT || '1000000000000000000' // 1 ether
const fee = bigInt(CELO_AMOUNT).shr(1) || bigInt(1e17)
const refund = CELO_AMOUNT || '1000000000000000000' // 1 ether
let recipient = getRandomRecipient()
const relayer = accounts[1]
let groth16
@ -72,6 +74,7 @@ contract('ERC20Tornado', accounts => {
token = await Token.deployed()
await token.mint(sender, tokenDenomination)
}
feeManager = await FeeManager.deployed()
badRecipient = await BadRecipient.new()
snapshotId = await takeSnapshot()
groth16 = await buildGroth16()
@ -191,6 +194,131 @@ contract('ERC20Tornado', accounts => {
isSpent.should.be.equal(true)
})
it("should give fees when feeTo is set", async () => {
await feeManager.setFeeTo(accounts[5]);
const balanceFeeToBefore = await token.balanceOf(accounts[5]);
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(tornado.address, tokenDenomination, { from: user });
// Uncomment to measure gas usage
// let gas = await tornado.deposit.estimateGas(toBN(deposit.commitment.toString()), { from: user, gasPrice: '0' })
// console.log('deposit gas:', gas)
await tornado.deposit(toFixedHex(deposit.commitment), {
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,
recipient,
fee,
refund,
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
});
const proofData = await websnarkUtils.genWitnessAndProve(
groth16,
input,
circuit,
proving_key
);
const { proof } = websnarkUtils.toSolidityInput(proofData);
const balanceTornadoBefore = await token.balanceOf(tornado.address);
const balanceRelayerBefore = await token.balanceOf(relayer);
const balanceRecieverBefore = await token.balanceOf(
toFixedHex(recipient, 20)
);
const ethBalanceOperatorBefore = await web3.eth.getBalance(operator);
const ethBalanceRecieverBefore = await web3.eth.getBalance(
toFixedHex(recipient, 20)
);
const ethBalanceRelayerBefore = await web3.eth.getBalance(relayer);
let isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash));
isSpent.should.be.equal(false);
// Uncomment to measure gas usage
// gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
// console.log('withdraw gas:', gas)
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 tornado.withdraw(proof, ...args, {
value: refund,
from: relayer,
gasPrice: "0",
});
const balanceTornadoAfter = await token.balanceOf(tornado.address);
const balanceRelayerAfter = await token.balanceOf(relayer);
const ethBalanceOperatorAfter = await web3.eth.getBalance(operator);
const balanceRecieverAfter = await token.balanceOf(
toFixedHex(recipient, 20)
);
const ethBalanceRecieverAfter = await web3.eth.getBalance(
toFixedHex(recipient, 20)
);
const ethBalanceRelayerAfter = await web3.eth.getBalance(relayer);
const balanceFeeToAfter = await token.balanceOf(accounts[5]);
const feeBN = toBN(fee.toString());
const feeToFee = toBN(tokenDenomination).div(toBN(200));
balanceTornadoAfter.should.be.eq.BN(
toBN(balanceTornadoBefore).sub(toBN(tokenDenomination))
);
balanceRelayerAfter.should.be.eq.BN(
toBN(balanceRelayerBefore).add(feeBN)
);
balanceRecieverAfter.should.be.eq.BN(
toBN(balanceRecieverBefore).add(
toBN(tokenDenomination).sub(feeBN).sub(feeToFee)
)
);
ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore));
ethBalanceRecieverAfter.should.be.eq.BN(
toBN(ethBalanceRecieverBefore).add(toBN(refund))
);
ethBalanceRelayerAfter.should.be.eq.BN(
toBN(ethBalanceRelayerBefore).sub(toBN(refund))
);
balanceFeeToAfter.sub(balanceFeeToBefore).should.be.eq.BN(feeToFee);
logs[0].event.should.be.equal("Withdrawal");
logs[0].args.nullifierHash.should.be.equal(
toFixedHex(input.nullifierHash)
);
logs[0].args.relayer.should.be.eq.BN(relayer);
logs[0].args.fee.should.be.eq.BN(feeBN);
isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash));
isSpent.should.be.equal(true);
});
it('should return refund to the relayer is case of fail', async () => {
const deposit = generateDeposit()
const user = accounts[4]

View File

@ -1,592 +0,0 @@
/* 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, randomHex } = require('web3-utils')
const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper')
const Tornado = artifacts.require('./ETHTornado.sol')
const { ETH_AMOUNT, MERKLE_TREE_HEIGHT } = process.env
const websnarkUtils = require('websnark/src/utils')
const buildGroth16 = require('websnark/src/groth16')
const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts
const unstringifyBigInts2 = require('snarkjs/src/stringifybigint').unstringifyBigInts
const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt
const crypto = require('crypto')
const circomlib = require('circomlib')
const MerkleTree = require('../lib/MerkleTree')
const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
const toFixedHex = (number, length = 32) => '0x' + bigInt(number).toString(16).padStart(length * 2, '0')
const getRandomRecipient = () => rbigint(20)
function generateDeposit() {
let deposit = {
secret: rbigint(31),
nullifier: rbigint(31),
}
const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
deposit.commitment = pedersenHash(preimage)
return deposit
}
// eslint-disable-next-line no-unused-vars
function BNArrayToStringArray(array) {
const arrayToPrint = []
array.forEach(item => {
arrayToPrint.push(item.toString())
})
return arrayToPrint
}
function snarkVerify(proof) {
proof = unstringifyBigInts2(proof)
const verification_key = unstringifyBigInts2(require('../build/circuits/withdraw_verification_key.json'))
return snarkjs['groth'].isValid(verification_key, proof, proof.publicSignals)
}
contract('ETHTornado', accounts => {
let tornado
const sender = accounts[0]
const operator = accounts[0]
const levels = MERKLE_TREE_HEIGHT || 16
const value = ETH_AMOUNT || '1000000000000000000' // 1 ether
let snapshotId
let prefix = 'test'
let tree
const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
const refund = bigInt(0)
const recipient = getRandomRecipient()
const relayer = accounts[1]
let groth16
let circuit
let proving_key
before(async () => {
tree = new MerkleTree(
levels,
null,
prefix,
)
tornado = await Tornado.deployed()
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 etherDenomination = await tornado.denomination()
etherDenomination.should.be.eq.BN(toBN(value))
})
})
describe('#deposit', () => {
it('should emit event', async () => {
let commitment = toFixedHex(42)
let { logs } = await tornado.deposit(commitment, { value, from: sender })
logs[0].event.should.be.equal('Deposit')
logs[0].args.commitment.should.be.equal(commitment)
logs[0].args.leafIndex.should.be.eq.BN(0)
commitment = toFixedHex(12);
({ logs } = await tornado.deposit(commitment, { value, from: accounts[2] }))
logs[0].event.should.be.equal('Deposit')
logs[0].args.commitment.should.be.equal(commitment)
logs[0].args.leafIndex.should.be.eq.BN(1)
})
it('should throw if there is a such commitment', async () => {
const commitment = toFixedHex(42)
await tornado.deposit(commitment, { value, from: sender }).should.be.fulfilled
const error = await tornado.deposit(commitment, { value, from: sender }).should.be.rejected
error.reason.should.be.equal('The commitment has been submitted')
})
})
describe('snark proof verification on js side', () => {
it('should detect tampering', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
const { root, path_elements, path_index } = await tree.path(0)
const input = stringifyBigInts({
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
})
let proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const originalProof = JSON.parse(JSON.stringify(proofData))
let result = snarkVerify(proofData)
result.should.be.equal(true)
// nullifier
proofData.publicSignals[1] = '133792158246920651341275668520530514036799294649489851421007411546007850802'
result = snarkVerify(proofData)
result.should.be.equal(false)
proofData = originalProof
// try to cheat with recipient
proofData.publicSignals[2] = '133738360804642228759657445999390850076318544422'
result = snarkVerify(proofData)
result.should.be.equal(false)
proofData = originalProof
// fee
proofData.publicSignals[3] = '1337100000000000000000'
result = snarkVerify(proofData)
result.should.be.equal(false)
proofData = originalProof
})
})
describe('#withdraw', () => {
it('should work', async () => {
const deposit = generateDeposit()
const user = accounts[4]
await tree.insert(deposit.commitment)
const balanceUserBefore = await web3.eth.getBalance(user)
// Uncomment to measure gas usage
// let gas = await tornado.deposit.estimateGas(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
// console.log('deposit gas:', gas)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: user, gasPrice: '0' })
const balanceUserAfter = await web3.eth.getBalance(user)
balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(value)))
const { root, path_elements, path_index } = await tree.path(0)
// Circuit input
const input = stringifyBigInts({
// public
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
relayer: operator,
recipient,
fee,
refund,
// private
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const balanceTornadoBefore = await web3.eth.getBalance(tornado.address)
const balanceRelayerBefore = await web3.eth.getBalance(relayer)
const balanceOperatorBefore = await web3.eth.getBalance(operator)
const balanceRecieverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20))
let isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(false)
// Uncomment to measure gas usage
// gas = await tornado.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
// console.log('withdraw gas:', gas)
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 tornado.withdraw(proof, ...args, { from: relayer, gasPrice: '0' })
const balanceTornadoAfter = await web3.eth.getBalance(tornado.address)
const balanceRelayerAfter = await web3.eth.getBalance(relayer)
const balanceOperatorAfter = await web3.eth.getBalance(operator)
const balanceRecieverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20))
const feeBN = toBN(fee.toString())
balanceTornadoAfter.should.be.eq.BN(toBN(balanceTornadoBefore).sub(toBN(value)))
balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
balanceOperatorAfter.should.be.eq.BN(toBN(balanceOperatorBefore).add(feeBN))
balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(value)).sub(feeBN))
logs[0].event.should.be.equal('Withdrawal')
logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash))
logs[0].args.relayer.should.be.eq.BN(operator)
logs[0].args.fee.should.be.eq.BN(feeBN)
isSpent = await tornado.isSpent(toFixedHex(input.nullifierHash))
isSpent.should.be.equal(true)
})
it('should prevent double spend', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const input = stringifyBigInts({
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
await tornado.withdraw(proof, ...args, { from: relayer }).should.be.fulfilled
const error = await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('The note has been already spent')
})
it('should prevent double spend with overflow', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const input = stringifyBigInts({
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const args = [
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 tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('verifier-gte-snark-scalar-field')
})
it('fee should be less or equal transfer value', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const largeFee = bigInt(value).add(bigInt(1))
const input = stringifyBigInts({
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee: largeFee,
refund,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
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 tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Fee exceeds transfer value')
})
it('should throw for corrupted merkle tree root', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const input = stringifyBigInts({
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
root,
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
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 tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Cannot find your merkle root')
})
it('should reject with tampered public inputs', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
let { root, path_elements, path_index } = await tree.path(0)
const input = stringifyBigInts({
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
let { proof } = websnarkUtils.toSolidityInput(proofData)
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()
// recipient
incorrectArgs = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex('0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337', 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
let error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof')
// fee
incorrectArgs = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex('0x000000000000000000000000000000000000000000000000015345785d8a0000'),
toFixedHex(input.refund)
]
error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof')
// nullifier
incorrectArgs = [
toFixedHex(input.root),
toFixedHex('0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad'),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
error = await tornado.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Invalid withdraw proof')
// proof itself
proof = '0xbeef' + proof.substr(6)
await tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
// should work with original values
await tornado.withdraw(originalProof, ...args, { from: relayer }).should.be.fulfilled
})
it('should reject with non zero refund', async () => {
const deposit = generateDeposit()
await tree.insert(deposit.commitment)
await tornado.deposit(toFixedHex(deposit.commitment), { value, from: sender })
const { root, path_elements, path_index } = await tree.path(0)
const input = stringifyBigInts({
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
root,
nullifier: deposit.nullifier,
relayer: operator,
recipient,
fee,
refund: bigInt(1),
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
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 tornado.withdraw(proof, ...args, { from: relayer }).should.be.rejected
error.reason.should.be.equal('Refund value is supposed to be zero for ETH instance')
})
})
describe('#changeOperator', () => {
it('should work', async () => {
let operator = await tornado.operator()
operator.should.be.equal(sender)
const newOperator = accounts[7]
await tornado.changeOperator(newOperator).should.be.fulfilled
operator = await tornado.operator()
operator.should.be.equal(newOperator)
})
it('cannot change from different address', async () => {
let operator = await tornado.operator()
operator.should.be.equal(sender)
const newOperator = accounts[7]
const error = await tornado.changeOperator(newOperator, { from: accounts[7] }).should.be.rejected
error.reason.should.be.equal('Only operator can call this function.')
})
})
describe('#updateVerifier', () => {
it('should work', async () => {
let operator = await tornado.operator()
operator.should.be.equal(sender)
const newVerifier = accounts[7]
await tornado.updateVerifier(newVerifier).should.be.fulfilled
const verifier = await tornado.verifier()
verifier.should.be.equal(newVerifier)
})
it('cannot change from different address', async () => {
let operator = await tornado.operator()
operator.should.be.equal(sender)
const newVerifier = accounts[7]
const error = await tornado.updateVerifier(newVerifier, { from: accounts[7] }).should.be.rejected
error.reason.should.be.equal('Only operator can call this function.')
})
})
describe('#isSpent', () => {
it('should work', async () => {
const deposit1 = generateDeposit()
const deposit2 = generateDeposit()
await tree.insert(deposit1.commitment)
await tree.insert(deposit2.commitment)
await tornado.deposit(toFixedHex(deposit1.commitment), { value, gasPrice: '0' })
await tornado.deposit(toFixedHex(deposit2.commitment), { value, gasPrice: '0' })
const { root, path_elements, path_index } = await tree.path(1)
// Circuit input
const input = stringifyBigInts({
// public
root,
nullifierHash: pedersenHash(deposit2.nullifier.leInt2Buff(31)),
relayer: operator,
recipient,
fee,
refund,
// private
nullifier: deposit2.nullifier,
secret: deposit2.secret,
pathElements: path_elements,
pathIndices: path_index,
})
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
const args = [
toFixedHex(input.root),
toFixedHex(input.nullifierHash),
toFixedHex(input.recipient, 20),
toFixedHex(input.relayer, 20),
toFixedHex(input.fee),
toFixedHex(input.refund)
]
await tornado.withdraw(proof, ...args, { from: relayer, gasPrice: '0' })
const nullifierHash1 = toFixedHex(pedersenHash(deposit1.nullifier.leInt2Buff(31)))
const nullifierHash2 = toFixedHex(pedersenHash(deposit2.nullifier.leInt2Buff(31)))
const spentArray = await tornado.isSpentArray([nullifierHash1, nullifierHash2])
spentArray.should.be.deep.equal([false, true])
})
})
afterEach(async () => {
await revertSnapshot(snapshotId.result)
// eslint-disable-next-line require-atomic-updates
snapshotId = await takeSnapshot()
tree = new MerkleTree(
levels,
null,
prefix,
)
})
})

View File

@ -10,6 +10,7 @@ const path = require('path')
const web3 = new Web3(process.env.RPC_URL)
const kit = ContractKit.newKitFromWeb3(web3)
kit.addAccount(process.env.PRIVATE_KEY)
// const kit = Kit.newKit('https://forno.celo.org') // mainnet endpoint
// const infuraKey = "fj4jll3k.....";
//
@ -84,11 +85,15 @@ module.exports = {
// CELO networks
alfajores: {
provider: kit.web3.currentProvider,
network_id: 44787
network_id: 44787,
gas: 6000000,
gasPrice: utils.toWei('0.1', 'gwei'),
},
mainnet: {
provider: kit.web3.currentProvider,
network_id: 42220
network_id: 42220,
gas: 6000000,
gasPrice: utils.toWei('0.1', 'gwei'),
}
// Useful for private networks