From 5e14553dfb0d50387e6f4f3e00cda06bc88ff78f Mon Sep 17 00:00:00 2001 From: Alexey Date: Thu, 30 Sep 2021 18:34:07 +0300 Subject: [PATCH] withdraw to L1 support and test --- .gitignore | 2 -- contracts/Mocks/MockOmniBridge.sol | 30 +++++++++++++++++ contracts/TornadoPool.sol | 14 +++++--- src/index.js | 4 +-- test/full.test.js | 53 ++++++++++++++++++++++++++++-- 5 files changed, 92 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 8737c62..b405677 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,4 @@ node_modules build cache artifacts -artifacts-ovm -cache-ovm src/types diff --git a/contracts/Mocks/MockOmniBridge.sol b/contracts/Mocks/MockOmniBridge.sol index 1ff04b0..bb2ef3a 100644 --- a/contracts/Mocks/MockOmniBridge.sol +++ b/contracts/Mocks/MockOmniBridge.sol @@ -17,4 +17,34 @@ contract MockOmniBridge is IOmniBridge { function execute(address _who, bytes calldata _calldata) external returns (bool success, bytes memory result) { (success, result) = _who.call(_calldata); } + + event OnTokenTransfer(address contr, address from, address receiver, uint256 value, bytes data); + + function onTokenTransfer( + address _from, + uint256 _value, + bytes memory _data + ) external returns (bool) { + bytes memory data = new bytes(0); + address receiver = _from; + if (_data.length >= 20) { + receiver = bytesToAddress(_data); + if (_data.length > 20) { + assembly { + let size := sub(mload(_data), 20) + data := add(_data, 20) + mstore(data, size) + } + } + } + emit OnTokenTransfer(msg.sender, _from, receiver, _value, data); + //bridgeSpecificActionsOnTokenTransfer(msg.sender, _from, receiver, _value, data); + return true; + } + + function bytesToAddress(bytes memory _bytes) internal pure returns (address addr) { + assembly { + addr := mload(add(_bytes, 20)) + } + } } diff --git a/contracts/TornadoPool.sol b/contracts/TornadoPool.sol index 20aaf25..8ae181d 100644 --- a/contracts/TornadoPool.sol +++ b/contracts/TornadoPool.sol @@ -16,6 +16,8 @@ pragma experimental ABIEncoderV2; import "@openzeppelin/contracts/contracts/token/ERC20/IERC20.sol"; import "./MerkleTreeWithHistory.sol"; +import "hardhat/console.sol"; + interface IERC6777 is IERC20 { function transferAndCall( address, @@ -47,6 +49,7 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver { IVerifier public immutable verifier16; IERC6777 public immutable token; address public immutable omniBridge; + address public immutable l1Unwrapper; struct ExtData { address recipient; @@ -55,7 +58,7 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver { uint256 fee; bytes encryptedOutput1; bytes encryptedOutput2; - bool isL1Withdraw; + bool isL1Withdrawal; } struct Proof { @@ -88,12 +91,14 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver { uint32 _levels, address _hasher, IERC6777 _token, - address _omniBridge + address _omniBridge, + address _l1Unwrapper ) MerkleTreeWithHistory(_levels, _hasher) { verifier2 = _verifier2; verifier16 = _verifier16; token = _token; omniBridge = _omniBridge; + l1Unwrapper = _l1Unwrapper; } function transact(Proof memory _args, ExtData memory _extData) public { @@ -120,13 +125,12 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver { if (_extData.extAmount < 0) { require(_extData.recipient != address(0), "Can't withdraw to zero address"); - if (_extData.isL1Withdraw) { - token.transferAndCall(omniBridge, uint256(-_extData.extAmount), abi.encode(_extData.recipient)); + if (_extData.isL1Withdrawal) { + token.transferAndCall(omniBridge, uint256(-_extData.extAmount), abi.encodePacked(l1Unwrapper, _extData.recipient)); } else { token.transfer(_extData.recipient, uint256(-_extData.extAmount)); } } - if (_extData.fee > 0) { token.transfer(_extData.relayer, _extData.fee); } diff --git a/src/index.js b/src/index.js index 41c69e7..a1afdda 100644 --- a/src/index.js +++ b/src/index.js @@ -146,7 +146,7 @@ async function transaction({ tornadoPool, ...rest }) { const receipt = await tornadoPool.transact(args, extData, { gasLimit: 1e6, }) - await receipt.wait() + return await receipt.wait() } async function registerAndTransact({ tornadoPool, packedPrivateKeyData, poolAddress, ...rest }) { @@ -166,4 +166,4 @@ async function registerAndTransact({ tornadoPool, packedPrivateKeyData, poolAddr await receipt.wait() } -module.exports = { transaction, registerAndTransact } +module.exports = { transaction, registerAndTransact, prepareTransaction } diff --git a/test/full.test.js b/test/full.test.js index 5b90fa6..3f85843 100644 --- a/test/full.test.js +++ b/test/full.test.js @@ -5,7 +5,7 @@ const { expect } = require('chai') const { utils } = ethers const Utxo = require('../src/utxo') -const { transaction, registerAndTransact } = require('../src/index') +const { transaction, registerAndTransact, prepareTransaction } = require('../src/index') const { Keypair } = require('../src/keypair') const MERKLE_TREE_HEIGHT = 5 @@ -21,7 +21,7 @@ describe('TornadoPool', function () { async function fixture() { require('../scripts/compileHasher') - const [sender, gov] = await ethers.getSigners() + const [sender, gov, l1Unwrapper] = await ethers.getSigners() const verifier2 = await deploy('Verifier2') const verifier16 = await deploy('Verifier16') const hasher = await deploy('Hasher') @@ -41,6 +41,7 @@ describe('TornadoPool', function () { hasher.address, token.address, omniBridge.address, + l1Unwrapper.address, ) await tornadoPool.initialize() @@ -211,6 +212,54 @@ describe('TornadoPool', function () { expect(bobBalance).to.be.equal(bobWithdrawAmount) }) + it('should deposit from L1 and withdraw to L1', async function () { + const { tornadoPool, token, omniBridge } = await loadFixture(fixture) + // console.log('tornadoPool', tornadoPool.interface) + + // Alice deposits into tornado pool + const aliceDepositAmount = 1e7 + const aliceDepositUtxo = new Utxo({ amount: aliceDepositAmount }) + const { args, extData } = await prepareTransaction({ + tornadoPool, + outputs: [aliceDepositUtxo], + }) + const transactTx = await tornadoPool.populateTransaction.registerAndTransact( + { pubKey: [], account: [] }, + args, + extData, + ) + const onTokenBridgedData = '0x' + transactTx.data.slice(10) + const onTokenBridgedTx = await tornadoPool.populateTransaction.onTokenBridged( + token.address, + aliceDepositUtxo.amount, + onTokenBridgedData, + ) + // emulating bridge. first it sends tokens and then calls onTokenBridged method + await token.transfer(tornadoPool.address, aliceDepositAmount) + await omniBridge.execute(tornadoPool.address, onTokenBridgedTx.data) + + // withdraws a part of his funds from the shielded pool + const aliceKeypair = new Keypair() // contains private and public keys + const aliceWithdrawAmount = 2e6 + const recipient = '0xDeaD00000000000000000000000000000000BEEf' + const aliceChangeUtxo = new Utxo({ + amount: aliceDepositAmount - aliceWithdrawAmount, + keypair: aliceKeypair, + }) + await transaction({ + tornadoPool, + inputs: [aliceDepositUtxo], + outputs: [aliceChangeUtxo], + recipient: recipient, + isL1Withdrawal: true, + }) + + const recipientBalance = await token.balanceOf(recipient) + expect(recipientBalance).to.be.equal(0) + const omniBridgeBalance = await token.balanceOf(omniBridge.address) + expect(omniBridgeBalance).to.be.equal(aliceWithdrawAmount) + }) + it('should work with 16 inputs', async function () { const { tornadoPool } = await loadFixture(fixture) await transaction({ tornadoPool, inputs: [new Utxo(), new Utxo(), new Utxo()] })