diff --git a/.env.example b/.env.example index c5c296e..55b74ea 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,12 @@ TOKEN_AMOUNT=100000000000000000 EMPTY_ELEMENT=1337 PRIVATE_KEY= ERC20_TOKEN= + +# DAI mirror in Kovan +#ERC20_TOKEN=0xd2b1a6b34f4a68425e7c28b4db5a37be3b7a4947 +# block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some DAI is 13146218 + +# USDT mirror in Kovan +#ERC20_TOKEN=0xf3e0d7bf58c5d455d31ef1c2d5375904df525105 +#TOKEN_AMOUNT=1000000 +# block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some USDT is 13147586 diff --git a/contracts/ERC20Mixer.sol b/contracts/ERC20Mixer.sol index aedbcea..d16a50b 100644 --- a/contracts/ERC20Mixer.sol +++ b/contracts/ERC20Mixer.sol @@ -12,10 +12,9 @@ pragma solidity ^0.5.8; import "./Mixer.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract ERC20Mixer is Mixer { - IERC20 public token; + address public token; // mixed token amount uint256 public tokenDenomination; // ether value to cover network fee (for relayer) and to have some ETH on a brand new address @@ -27,7 +26,7 @@ contract ERC20Mixer is Mixer { uint8 _merkleTreeHeight, uint256 _emptyElement, address payable _operator, - IERC20 _token, + address _token, uint256 _tokenDenomination ) Mixer(_verifier, _merkleTreeHeight, _emptyElement, _operator) public { token = _token; @@ -42,7 +41,7 @@ contract ERC20Mixer is Mixer { */ function deposit(uint256 commitment) public payable { require(msg.value == etherFeeDenomination, "Please send `etherFeeDenomination` ETH along with transaction"); - require(token.transferFrom(msg.sender, address(this), tokenDenomination), "Approve before using"); + transferFrom(msg.sender, address(this), tokenDenomination); _deposit(commitment); emit Deposit(commitment, next_index - 1, block.timestamp); @@ -69,8 +68,46 @@ contract ERC20Mixer is Mixer { operator.transfer(fee); } - token.transfer(receiver, tokenDenomination); + transfer(receiver, tokenDenomination); emit Withdraw(receiver, nullifierHash, fee); } + + function transferFrom(address from, address to, uint256 amount) internal { + bool success; + bytes memory data; + bytes4 transferFromSelector = 0x23b872dd; + (success, data) = token.call( + abi.encodeWithSelector( + transferFromSelector, + from, to, amount + ) + ); + require(success, "not enough allowed tokens"); + if (data.length > 0) { + assembly { + success := mload(add(data, 0x20)) + } + require(success, "not enough allowed tokens"); + } + } + + function transfer(address to, uint256 amount) internal { + bool success; + bytes memory data; + bytes4 transferSelector = 0xa9059cbb; + (success, data) = token.call( + abi.encodeWithSelector( + transferSelector, + to, amount + ) + ); + require(success, "not enough tokens"); + if (data.length > 0) { + assembly { + success := mload(add(data, 0x20)) + } + require(success, "not enough tokens"); + } + } } diff --git a/contracts/Mocks/IUSDT.sol b/contracts/Mocks/IUSDT.sol new file mode 100644 index 0000000..8bd9ead --- /dev/null +++ b/contracts/Mocks/IUSDT.sol @@ -0,0 +1,18 @@ +contract ERC20Basic { + uint public _totalSupply; + function totalSupply() public view returns (uint); + function balanceOf(address who) public view returns (uint); + function transfer(address to, uint value) public; + event Transfer(address indexed from, address indexed to, uint value); +} + +/** + * @title ERC20 interface + * @dev see https://github.com/ethereum/EIPs/issues/20 + */ +contract IUSDT is ERC20Basic { + function allowance(address owner, address spender) public view returns (uint); + function transferFrom(address from, address to, uint value) public; + function approve(address spender, uint value) public; + event Approval(address indexed owner, address indexed spender, uint value); +} diff --git a/migrations/5_deploy_erc20_mixer.js b/migrations/5_deploy_erc20_mixer.js index b710d9e..8ee907c 100644 --- a/migrations/5_deploy_erc20_mixer.js +++ b/migrations/5_deploy_erc20_mixer.js @@ -13,7 +13,7 @@ module.exports = function(deployer, network, accounts) { const miMC = await MiMC.deployed() await ERC20Mixer.link(MiMC, miMC.address) let token = ERC20_TOKEN - if(deployer.network !== 'mainnet') { + if(token === '') { const tokenInstance = await deployer.deploy(ERC20Mock) token = tokenInstance.address } diff --git a/test/ERC20Mixer.test.js b/test/ERC20Mixer.test.js index 6d9aab2..a0432cd 100644 --- a/test/ERC20Mixer.test.js +++ b/test/ERC20Mixer.test.js @@ -10,7 +10,8 @@ const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper') const Mixer = artifacts.require('./ERC20Mixer.sol') const Token = artifacts.require('./ERC20Mock.sol') -const { ETH_AMOUNT, TOKEN_AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env +const USDTToken = artifacts.require('./IUSDT.sol') +const { ETH_AMOUNT, TOKEN_AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, ERC20_TOKEN } = process.env const websnarkUtils = require('websnark/src/utils') const buildGroth16 = require('websnark/src/groth16') @@ -45,11 +46,12 @@ function getRandomReceiver() { contract('ERC20Mixer', accounts => { let mixer let token + let usdtToken const sender = accounts[0] const operator = accounts[0] const levels = MERKLE_TREE_HEIGHT || 16 const zeroValue = EMPTY_ELEMENT || 1337 - const tokenDenomination = TOKEN_AMOUNT || '1000000000000000000' // 1 ether + let tokenDenomination = TOKEN_AMOUNT || '1000000000000000000' // 1 ether const value = ETH_AMOUNT || '1000000000000000000' // 1 ether let snapshotId let prefix = 'test' @@ -69,8 +71,13 @@ contract('ERC20Mixer', accounts => { prefix, ) mixer = await Mixer.deployed() - token = await Token.deployed() - await token.mint(sender, tokenDenomination) + if (ERC20_TOKEN) { + token = await Token.at(ERC20_TOKEN) + usdtToken = await USDTToken.at(ERC20_TOKEN) + } else { + token = await Token.deployed() + await token.mint(sender, tokenDenomination) + } snapshotId = await takeSnapshot() groth16 = await buildGroth16() circuit = require('../build/circuits/withdraw.json') @@ -161,6 +168,167 @@ contract('ERC20Mixer', accounts => { ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).add(toBN(value)).sub(feeBN)) + logs[0].event.should.be.equal('Withdraw') + logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString())) + logs[0].args.fee.should.be.eq.BN(feeBN) + isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) + isSpent.should.be.equal(true) + }) + + it.skip('should work with REAL USDT', async () => { + // dont forget to specify your token in .env + // USDT decimals is 6, so TOKEN_AMOUNT=1000000 + // and sent `tokenDenomination` to accounts[0] (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1) + // run ganache as + // ganache-cli --fork https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448@13147586 -d --keepAliveTimeout 20 + const deposit = generateDeposit() + const user = accounts[4] + const userBal = await usdtToken.balanceOf(user) + console.log('userBal', userBal.toString()) + const senderBal = await usdtToken.balanceOf(sender) + console.log('senderBal', senderBal.toString()) + await tree.insert(deposit.commitment) + await usdtToken.transfer(user, tokenDenomination, { from: sender }) + console.log('transfer done') + + const balanceUserBefore = await usdtToken.balanceOf(user) + console.log('balanceUserBefore', balanceUserBefore.toString()) + await usdtToken.approve(mixer.address, tokenDenomination, { from: user }) + console.log('approve done') + const allowanceUser = await usdtToken.allowance(user, mixer.address) + console.log('allowanceUser', allowanceUser.toString()) + await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' }) + console.log('deposit done') + + const balanceUserAfter = await usdtToken.balanceOf(user) + balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination))) + + const { root, path_elements, path_index } = await tree.path(0) + + // Circuit input + const input = stringifyBigInts({ + // public + root, + nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), + receiver, + fee, + + // private + nullifier: deposit.nullifier, + secret: deposit.secret, + pathElements: path_elements, + pathIndex: path_index, + }) + + + const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) + const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof) + + const balanceMixerBefore = await usdtToken.balanceOf(mixer.address) + const balanceRelayerBefore = await usdtToken.balanceOf(relayer) + const ethBalanceOperatorBefore = await web3.eth.getBalance(operator) + const balanceRecieverBefore = await usdtToken.balanceOf(toHex(receiver.toString())) + const ethBalanceRecieverBefore = await web3.eth.getBalance(toHex(receiver.toString())) + let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) + isSpent.should.be.equal(false) + + // Uncomment to measure gas usage + // gas = await mixer.withdraw.estimateGas(pi_a, pi_b, pi_c, publicSignals, { from: relayer, gasPrice: '0' }) + // console.log('withdraw gas:', gas) + const { logs } = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer, gasPrice: '0' }) + + const balanceMixerAfter = await usdtToken.balanceOf(mixer.address) + const balanceRelayerAfter = await usdtToken.balanceOf(relayer) + const ethBalanceOperatorAfter = await web3.eth.getBalance(operator) + const balanceRecieverAfter = await usdtToken.balanceOf(toHex(receiver.toString())) + const ethBalanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString())) + const feeBN = toBN(fee.toString()) + balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination))) + balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore)) + ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore).add(feeBN)) + balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(tokenDenomination))) + ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).add(toBN(value)).sub(feeBN)) + + + logs[0].event.should.be.equal('Withdraw') + logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString())) + logs[0].args.fee.should.be.eq.BN(feeBN) + isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) + isSpent.should.be.equal(true) + }) + it.skip('should work with REAL DAI', async () => { + // dont forget to specify your token in .env + // and sent `tokenDenomination` to accounts[0] (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1) + // run ganache as + // ganache-cli --fork https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448@13146218 -d --keepAliveTimeout 20 + const deposit = generateDeposit() + const user = accounts[4] + const userBal = await token.balanceOf(user) + console.log('userBal', userBal.toString()) + const senderBal = await token.balanceOf(sender) + console.log('senderBal', senderBal.toString()) + await tree.insert(deposit.commitment) + await token.transfer(user, tokenDenomination, { from: sender }) + console.log('transfer done') + + const balanceUserBefore = await token.balanceOf(user) + console.log('balanceUserBefore', balanceUserBefore.toString()) + await token.approve(mixer.address, tokenDenomination, { from: user }) + console.log('approve done') + await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' }) + console.log('deposit done') + + const balanceUserAfter = await token.balanceOf(user) + balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(tokenDenomination))) + + const { root, path_elements, path_index } = await tree.path(0) + + // Circuit input + const input = stringifyBigInts({ + // public + root, + nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), + receiver, + fee, + + // private + nullifier: deposit.nullifier, + secret: deposit.secret, + pathElements: path_elements, + pathIndex: path_index, + }) + + + const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) + const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof) + + const balanceMixerBefore = await token.balanceOf(mixer.address) + const balanceRelayerBefore = await token.balanceOf(relayer) + const ethBalanceOperatorBefore = await web3.eth.getBalance(operator) + const balanceRecieverBefore = await token.balanceOf(toHex(receiver.toString())) + const ethBalanceRecieverBefore = await web3.eth.getBalance(toHex(receiver.toString())) + let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) + isSpent.should.be.equal(false) + + // Uncomment to measure gas usage + // gas = await mixer.withdraw.estimateGas(pi_a, pi_b, pi_c, publicSignals, { from: relayer, gasPrice: '0' }) + // console.log('withdraw gas:', gas) + const { logs } = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer, gasPrice: '0' }) + console.log('withdraw done') + + const balanceMixerAfter = await token.balanceOf(mixer.address) + const balanceRelayerAfter = await token.balanceOf(relayer) + const ethBalanceOperatorAfter = await web3.eth.getBalance(operator) + const balanceRecieverAfter = await token.balanceOf(toHex(receiver.toString())) + const ethBalanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString())) + const feeBN = toBN(fee.toString()) + balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(tokenDenomination))) + balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore)) + ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore).add(feeBN)) + balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(tokenDenomination))) + ethBalanceRecieverAfter.should.be.eq.BN(toBN(ethBalanceRecieverBefore).add(toBN(value)).sub(feeBN)) + + logs[0].event.should.be.equal('Withdraw') logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString())) logs[0].args.fee.should.be.eq.BN(feeBN)