From ca5f4da72dc52014e5c076fec5c91593c9e9795a Mon Sep 17 00:00:00 2001 From: Drygin Date: Mon, 23 May 2022 23:35:18 +0300 Subject: [PATCH] add transparent deposit logic --- config.js | 1 + contracts/TornadoPool.sol | 27 +++++++++++++ contracts/interfaces/IHasher3.sol | 6 +++ scripts/compileHasher3.js | 22 +++++++++++ src/0_generateAddresses.js | 1 + test/full.test.js | 65 +++++++++++++++++++++++++++++++ 6 files changed, 122 insertions(+) create mode 100644 contracts/interfaces/IHasher3.sol create mode 100644 scripts/compileHasher3.js diff --git a/config.js b/config.js index 669bb3e..bfb1074 100644 --- a/config.js +++ b/config.js @@ -17,6 +17,7 @@ module.exports = { verifier16: '0x743494b60097a2230018079c02fe21a7b687eaa5', MERKLE_TREE_HEIGHT: 23, hasher: '0x94c92f096437ab9958fc0a37f09348f30389ae79', + hasher3: '0x94c92f096437ab9958fc0a37f09348f30389ae79', // TODO gcWeth: '0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1', gcOmniBridge: '0xf6a78083ca3e2a662d6dd1703c939c8ace2e268d', l1Unwrapper: '0x3F615bA21Bc6Cc5D4a6D798c5950cc5c42937fbd', diff --git a/contracts/TornadoPool.sol b/contracts/TornadoPool.sol index 3e2d5a6..842becb 100644 --- a/contracts/TornadoPool.sol +++ b/contracts/TornadoPool.sol @@ -17,6 +17,7 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import { IERC20Receiver, IERC6777, IOmniBridge } from "./interfaces/IBridge.sol"; import { CrossChainGuard } from "./bridge/CrossChainGuard.sol"; import { IVerifier } from "./interfaces/IVerifier.sol"; +import { IHasher3 } from "./interfaces/IHasher3.sol"; import "./MerkleTreeWithHistory.sol"; /** @dev This contract(pool) allows deposit of an arbitrary amount to it, shielded transfer to another registered user inside the pool @@ -28,6 +29,7 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, IVerifier public immutable verifier2; IVerifier public immutable verifier16; + IHasher3 public immutable hasher3; IERC6777 public immutable token; address public immutable omniBridge; address public immutable l1Unwrapper; @@ -78,6 +80,7 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, @param _verifier16 the address of SNARK verifier for 16 inputs @param _levels hight of the commitments merkle tree @param _hasher hasher address for the merkle tree + @param _hasher3 hasher address for the commitment @param _token token address for the pool @param _omniBridge omniBridge address for specified token @param _l1Unwrapper address of the L1Helper @@ -90,6 +93,7 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, IVerifier _verifier16, uint32 _levels, address _hasher, + address _hasher3, IERC6777 _token, address _omniBridge, address _l1Unwrapper, @@ -102,6 +106,7 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, { verifier2 = _verifier2; verifier16 = _verifier16; + hasher3 = IHasher3(_hasher3); token = _token; omniBridge = _omniBridge; l1Unwrapper = _l1Unwrapper; @@ -125,6 +130,28 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, _transact(_args, _extData); } + /** @dev Function that allows transparent deposits without proof verification. + */ + function transparentDeposit(bytes32 pubkey, uint256 depositAmount) public payable { + require(depositAmount <= maximumDepositAmount, "amount is larger than maximumDepositAmount"); + require(token.transferFrom(msg.sender, address(this), depositAmount), "transfer failed"); + + require(depositAmount < FIELD_SIZE, "depositAmount should be inside the field"); + require(uint256(pubkey) < FIELD_SIZE, "pubkey should be inside the field"); + bytes32[3] memory input; + input[0] = bytes32(depositAmount); + input[1] = pubkey; + input[2] = bytes32(0); + bytes32 commitment = hasher3.poseidon(input); + + bytes memory packedOutput = abi.encodePacked("abi", depositAmount, pubkey); + + lastBalance = token.balanceOf(address(this)); + _insert(commitment, bytes32(0)); // use second empty commitment for better efficiency + emit NewCommitment(commitment, nextIndex - 2, packedOutput); + emit NewCommitment(bytes32(0), nextIndex - 1, abi.encodePacked("gap")); + } + function register(Account memory _account) public { require(_account.owner == msg.sender, "only owner can be registered"); _register(_account); diff --git a/contracts/interfaces/IHasher3.sol b/contracts/interfaces/IHasher3.sol new file mode 100644 index 0000000..94b6bf4 --- /dev/null +++ b/contracts/interfaces/IHasher3.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +interface IHasher3 { + function poseidon(bytes32[3] calldata inputs) external pure returns (bytes32); +} diff --git a/scripts/compileHasher3.js b/scripts/compileHasher3.js new file mode 100644 index 0000000..0076dc3 --- /dev/null +++ b/scripts/compileHasher3.js @@ -0,0 +1,22 @@ +// Generates Hasher artifact at compile-time using external compilermechanism +const path = require('path') +const fs = require('fs') +const genContract = require('circomlib/src/poseidon_gencontract.js') +const outputPath = path.join(__dirname, '..', 'artifacts', 'contracts') +const outputFile = path.join(outputPath, 'Hasher3.json') + +if (!fs.existsSync(outputPath)) { + fs.mkdirSync(outputPath, { recursive: true }) +} + +const contract = { + _format: 'hh-sol-artifact-1', + sourceName: 'contracts/Hasher.sol', + linkReferences: {}, + deployedLinkReferences: {}, + contractName: 'Hasher3', + abi: genContract.generateABI(3), + bytecode: genContract.createCode(3), +} + +fs.writeFileSync(outputFile, JSON.stringify(contract, null, 2)) diff --git a/src/0_generateAddresses.js b/src/0_generateAddresses.js index 27ad3b3..bc81f37 100644 --- a/src/0_generateAddresses.js +++ b/src/0_generateAddresses.js @@ -24,6 +24,7 @@ async function generate(config = defaultConfig) { config.verifier16, config.MERKLE_TREE_HEIGHT, config.hasher, + config.hasher3, config.gcWeth, config.gcOmniBridge, config.l1Unwrapper, diff --git a/test/full.test.js b/test/full.test.js index 28ea23d..506d3be 100644 --- a/test/full.test.js +++ b/test/full.test.js @@ -3,6 +3,7 @@ const { ethers, waffle } = hre const { loadFixture } = waffle const { expect } = require('chai') const { utils } = ethers +const { BigNumber } = require('@ethersproject/bignumber') const Utxo = require('../src/utxo') const { transaction, registerAndTransact, prepareTransaction, buildMerkleTree } = require('../src/index') @@ -27,10 +28,12 @@ describe('TornadoPool', function () { async function fixture() { require('../scripts/compileHasher') + require('../scripts/compileHasher3') const [sender, gov, multisig] = await ethers.getSigners() const verifier2 = await deploy('Verifier2') const verifier16 = await deploy('Verifier16') const hasher = await deploy('Hasher') + const hasher3 = await deploy('Hasher3') const token = await deploy('PermittableToken', 'Wrapped ETH', 'WETH', 18, l1ChainId) await token.mint(sender.address, utils.parseEther('10000')) @@ -59,6 +62,7 @@ describe('TornadoPool', function () { verifier16.address, MERKLE_TREE_HEIGHT, hasher.address, + hasher3.address, token.address, omniBridge.address, l1Unwrapper.address, @@ -175,6 +179,67 @@ describe('TornadoPool', function () { expect(registerEvent.args.key).to.be.equal(aliceDepositUtxo.keypair.address()) }) + it('should transparent deposit', async function () { + let { tornadoPool, token } = await loadFixture(fixture) + const sender = (await ethers.getSigners())[0] + tornadoPool = tornadoPool.connect(sender) + + // Alice deposits into tornado pool + const aliceDepositAmount = utils.parseEther('0.1') + const aliceKeypair = new Keypair() + const alicePubkey = aliceKeypair.address().slice(0, 66) + + await expect(() => + tornadoPool.transparentDeposit(alicePubkey, aliceDepositAmount), + ).to.changeTokenBalances( + token, + [sender, tornadoPool], + [BigNumber.from(0).sub(aliceDepositAmount), aliceDepositAmount], + ) + + const filter = tornadoPool.filters.NewCommitment() + let fromBlock = await ethers.provider.getBlock() + let events = await tornadoPool.queryFilter(filter, fromBlock.number) + + const packedOutput = utils.solidityPack( + ['string', 'uint256', 'bytes32'], + ['abi', aliceDepositAmount, alicePubkey], + ) + expect(events[0].args.encryptedOutput).to.be.equal(packedOutput) + + const aliceDepositUtxo = new Utxo({ + amount: aliceDepositAmount, + keypair: aliceKeypair, + blinding: 0, + index: 0, + }) + + // Bob gives Alice address to send some eth inside the shielded pool + const bobKeypair = new Keypair() // contains private and public keys + const bobAddress = bobKeypair.address() // contains only public key + + // Alice sends some funds to Bob + const bobSendAmount = utils.parseEther('0.06') + const bobSendUtxo = new Utxo({ amount: bobSendAmount, keypair: Keypair.fromString(bobAddress) }) + const aliceChangeUtxo = new Utxo({ + amount: aliceDepositAmount.sub(bobSendAmount), + keypair: aliceDepositUtxo.keypair, + }) + await transaction({ tornadoPool, inputs: [aliceDepositUtxo], outputs: [bobSendUtxo, aliceChangeUtxo] }) + + // Bob parses chain to detect incoming funds + fromBlock = await ethers.provider.getBlock() + events = await tornadoPool.queryFilter(filter, fromBlock.number) + let bobReceiveUtxo + try { + bobReceiveUtxo = Utxo.decrypt(bobKeypair, events[0].args.encryptedOutput, events[0].args.index) + } catch (e) { + // we try to decrypt another output here because it shuffles outputs before sending to blockchain + bobReceiveUtxo = Utxo.decrypt(bobKeypair, events[1].args.encryptedOutput, events[1].args.index) + } + expect(bobReceiveUtxo.amount).to.be.equal(bobSendAmount) + }) + it('should deposit, transact and withdraw', async function () { const { tornadoPool, token } = await loadFixture(fixture)