diff --git a/.env.example b/.env.example index a6eed85..55e2c41 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,6 @@ XDAI_RPC=https:// BSC_RPC=https:// MINIMUM_WITHDRAWAL_AMOUNT=0.05 MAXIMUM_DEPOSIT_AMOUNT=1 +ALCHEMY_KEY= +INFURA_API_KEY= +ETHERSCAN_KEY= diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4ed858d..4d73f80 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,8 @@ jobs: - run: yarn lint - run: yarn build - run: yarn test + env: + ALCHEMY_KEY: ${{ secrets.ALCHEMY_KEY }} - name: Telegram Failure Notification uses: appleboy/telegram-action@0.0.7 if: failure() diff --git a/.gitignore b/.gitignore index b405677..050401f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build cache artifacts src/types +.vscode diff --git a/.solhint.json b/.solhint.json index b313abe..d516078 100644 --- a/.solhint.json +++ b/.solhint.json @@ -7,7 +7,8 @@ "printWidth": 110 } ], - "quotes": ["error", "double"] + "quotes": ["error", "double"], + "compiler-version": ["error", "^0.7.0"] }, "plugins": ["prettier"] } diff --git a/README.md b/README.md index 448db99..631f2f7 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,31 @@ yarn download yarn build yarn test ``` + +## Deploy + +Check config.js for actual values. + +With `salt` = `0x0000000000000000000000000000000000000000000000000000000047941987` addresses must be: + +1. `L1Unwrapper` - `0x3F615bA21Bc6Cc5D4a6D798c5950cc5c42937fbd` +2. `TornadoPool` - `0x0CDD3705aF7979fBe80A64288Ebf8A9Fe1151cE1` + +Check addresses with current config: + +```shell +yarn compile +node -e 'require("./src/0_generateAddresses").generateWithLog()' +``` + +Deploy L1Unwrapper: + +```shell +npx hardhat run scripts/deployL1Unwrapper.js --network mainnet +``` + +Deploy TornadoPool Upgrade: + +```shell +npx hardhat run scripts/deployTornadoUpgrade.js --network xdai +``` diff --git a/config.js b/config.js new file mode 100644 index 0000000..669bb3e --- /dev/null +++ b/config.js @@ -0,0 +1,26 @@ +module.exports = { + //// L1 ------------------- + // ETH + multisig: '0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4', + omniBridge: '0x88ad09518695c6c3712AC10a214bE5109a655671', + weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + // BSC + // multisig: '0xBAE5aBfa98466Dbe68836763B087f2d189f4D28f' + // omniBridge: '0xf0b456250dc9990662a6f25808cc74a6d1131ea9' + // weth: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c' // WBNB + singletonFactory: '0xce0042B868300000d44A59004Da54A005ffdcf9f', + salt: '0x0000000000000000000000000000000000000000000000000000000047941987', + + //// L2 ------------------- + // Gnosis chain + verifier2: '0xdf3a408c53e5078af6e8fb2a85088d46ee09a61b', + verifier16: '0x743494b60097a2230018079c02fe21a7b687eaa5', + MERKLE_TREE_HEIGHT: 23, + hasher: '0x94c92f096437ab9958fc0a37f09348f30389ae79', + gcWeth: '0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1', + gcOmniBridge: '0xf6a78083ca3e2a662d6dd1703c939c8ace2e268d', + l1Unwrapper: '0x3F615bA21Bc6Cc5D4a6D798c5950cc5c42937fbd', + govAddress: '0x5efda50f22d34f262c29268506c5fa42cb56a1ce', + l1ChainId: 1, + gcMultisig: '0x1f727de610030a88863d7da45bdea4eb84655b52', +} diff --git a/contracts/Mocks/WETH.sol b/contracts/Mocks/WETH.sol new file mode 100644 index 0000000..21231ef --- /dev/null +++ b/contracts/Mocks/WETH.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract WETH is ERC20 { + constructor(string memory name, string memory ticker) ERC20(name, ticker) {} + + function deposit() external payable { + _mint(msg.sender, msg.value); + } + + function withdraw(uint256 value) external { + _burn(msg.sender, value); + (bool success, ) = msg.sender.call{ value: value }(""); + require(success, "WETH: ETH transfer failed"); + } +} diff --git a/contracts/TornadoPool.sol b/contracts/TornadoPool.sol index bdf379d..8744774 100644 --- a/contracts/TornadoPool.sol +++ b/contracts/TornadoPool.sol @@ -25,6 +25,7 @@ import "./MerkleTreeWithHistory.sol"; contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, CrossChainGuard { int256 public constant MAX_EXT_AMOUNT = 2**248; uint256 public constant MAX_FEE = 2**248; + uint256 public constant MIN_EXT_AMOUNT_LIMIT = 0.5 ether; IVerifier public immutable verifier2; IVerifier public immutable verifier16; @@ -34,7 +35,7 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, address public immutable multisig; uint256 public lastBalance; - uint256 public minimalWithdrawalAmount; + uint256 public __gap; // storage padding to prevent storage collision uint256 public maximumDepositAmount; mapping(bytes32 => bool) public nullifierHashes; @@ -46,6 +47,7 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, bytes encryptedOutput1; bytes encryptedOutput2; bool isL1Withdrawal; + uint256 l1Fee; } struct Proof { @@ -66,11 +68,6 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, event NewNullifier(bytes32 nullifier); event PublicKey(address indexed owner, bytes key); - modifier onlyGovernance() { - require(isCalledByOwner(), "only governance"); - _; - } - modifier onlyMultisig() { require(msg.sender == multisig, "only governance"); _; @@ -112,8 +109,8 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, multisig = _multisig; } - function initialize(uint256 _minimalWithdrawalAmount, uint256 _maximumDepositAmount) external initializer { - _configureLimits(_minimalWithdrawalAmount, _maximumDepositAmount); + function initialize(uint256 _maximumDepositAmount) external initializer { + _configureLimits(_maximumDepositAmount); super._initialize(); } @@ -191,8 +188,8 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, } } - function configureLimits(uint256 _minimalWithdrawalAmount, uint256 _maximumDepositAmount) public onlyGovernance { - _configureLimits(_minimalWithdrawalAmount, _maximumDepositAmount); + function configureLimits(uint256 _maximumDepositAmount) public onlyMultisig { + _configureLimits(_maximumDepositAmount); } function calculatePublicAmount(int256 _extAmount, uint256 _fee) public pure returns (uint256) { @@ -275,11 +272,14 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, if (_extData.extAmount < 0) { require(_extData.recipient != address(0), "Can't withdraw to zero address"); if (_extData.isL1Withdrawal) { - token.transferAndCall(omniBridge, uint256(-_extData.extAmount), abi.encodePacked(l1Unwrapper, _extData.recipient)); + token.transferAndCall( + omniBridge, + uint256(-_extData.extAmount), + abi.encodePacked(l1Unwrapper, abi.encode(_extData.recipient, _extData.l1Fee)) + ); } else { token.transfer(_extData.recipient, uint256(-_extData.extAmount)); } - require(uint256(-_extData.extAmount) >= minimalWithdrawalAmount, "amount is less than minimalWithdrawalAmount"); // prevents ddos attack to Bridge } if (_extData.fee > 0) { token.transfer(_extData.relayer, _extData.fee); @@ -294,8 +294,7 @@ contract TornadoPool is MerkleTreeWithHistory, IERC20Receiver, ReentrancyGuard, } } - function _configureLimits(uint256 _minimalWithdrawalAmount, uint256 _maximumDepositAmount) internal { - minimalWithdrawalAmount = _minimalWithdrawalAmount; + function _configureLimits(uint256 _maximumDepositAmount) internal { maximumDepositAmount = _maximumDepositAmount; } } diff --git a/contracts/bridge/L1Helper.sol b/contracts/bridge/L1Unwrapper.sol similarity index 55% rename from contracts/bridge/L1Helper.sol rename to contracts/bridge/L1Unwrapper.sol index 4526c93..07491bc 100644 --- a/contracts/bridge/L1Helper.sol +++ b/contracts/bridge/L1Unwrapper.sol @@ -14,9 +14,19 @@ pragma solidity ^0.7.0; pragma abicoder v2; import "omnibridge/contracts/helpers/WETHOmnibridgeRouter.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; /// @dev Extension for original WETHOmnibridgeRouter that stores TornadoPool account registrations. -contract L1Helper is WETHOmnibridgeRouter { +contract L1Unwrapper is WETHOmnibridgeRouter { + using SafeMath for uint256; + + // If this address sets to not zero it receives L1_fee. + // It can be changed by the multisig. + // And should implement fee sharing logic: + // - some part to tx.origin - based on block base fee and can be subsidized + // - store surplus of ETH for future subsidizions + address payable public l1FeeReceiver; + event PublicKey(address indexed owner, bytes key); struct Account { @@ -61,4 +71,42 @@ contract L1Helper is WETHOmnibridgeRouter { function _register(Account memory _account) internal { emit PublicKey(_account.owner, _account.publicKey); } + + /** + * @dev Bridged callback function used for unwrapping received tokens. + * Can only be called by the associated Omnibridge contract. + * @param _token bridged token contract address, should be WETH. + * @param _value amount of bridged/received tokens. + * @param _data extra data passed alongside with relayTokensAndCall on the other side of the bridge. + * Should contain coins receiver address and L1 executer fee amount. + */ + function onTokenBridged( + address _token, + uint256 _value, + bytes memory _data + ) external override { + require(_token == address(WETH), "only WETH token"); + require(msg.sender == address(bridge), "only from bridge address"); + require(_data.length == 64, "incorrect data length"); + + WETH.withdraw(_value); + + (address payable receipient, uint256 l1Fee) = abi.decode(_data, (address, uint256)); + + AddressHelper.safeSendValue(receipient, _value.sub(l1Fee)); + + if (l1Fee > 0) { + address payable l1FeeTo = l1FeeReceiver != payable(address(0)) ? l1FeeReceiver : payable(tx.origin); + AddressHelper.safeSendValue(l1FeeTo, l1Fee); + } + } + + /** + * @dev Sets l1FeeReceiver address. + * Only contract owner can call this method. + * @param _receiver address of new L1FeeReceiver, address(0) for native tx.origin receiver. + */ + function setL1FeeReceiver(address payable _receiver) external onlyOwner { + l1FeeReceiver = _receiver; + } } diff --git a/contracts/libraries/SingletonFactory.sol b/contracts/libraries/SingletonFactory.sol new file mode 100644 index 0000000..13653e5 --- /dev/null +++ b/contracts/libraries/SingletonFactory.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +/** + *Submitted for verification at Etherscan.io on 2020-03-30 + */ + +pragma solidity 0.6.2; + +/** + * @title Singleton Factory (EIP-2470) + * @notice Exposes CREATE2 (EIP-1014) to deploy bytecode on deterministic addresses based on initialization code and salt. + * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) + */ +contract SingletonFactory { + /** + * @notice Deploys `_initCode` using `_salt` for defining the deterministic address. + * @param _initCode Initialization code. + * @param _salt Arbitrary value to modify resulting address. + * @return createdContract Created contract address. + */ + function deploy(bytes memory _initCode, bytes32 _salt) public returns (address payable createdContract) { + assembly { + createdContract := create2(0, add(_initCode, 0x20), mload(_initCode), _salt) + } + } +} +// IV is a value changed to generate the vanity address. +// IV: 6583047 diff --git a/hardhat.config.js b/hardhat.config.js index 2a1f3c9..304daed 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -21,6 +21,15 @@ const config = { }, }, }, + { + version: '0.6.2', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, { version: '0.7.5', settings: { @@ -42,6 +51,25 @@ const config = { ], }, networks: { + hardhat: { + forking: { + url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`, + blockNumber: 13685625, + }, + chainId: 1, + initialBaseFeePerGas: 5, + loggingEnabled: false, + allowUnlimitedContractSize: false, + blockGasLimit: 50000000, + }, + rinkeby: { + url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`, + accounts: process.env.PRIVATE_KEY + ? [process.env.PRIVATE_KEY] + : { + mnemonic: 'test test test test test test test test test test test junk', + }, + }, xdai: { url: process.env.ETH_RPC || 'https://rpc.xdaichain.com/', accounts: process.env.PRIVATE_KEY @@ -60,7 +88,7 @@ const config = { }, }, mainnet: { - url: process.env.ETH_RPC || '', + url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`, accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : { diff --git a/package.json b/package.json index e56513c..b329627 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "fixed-merkle-tree": "^0.5.1", "hardhat": "^2.3.0", "mocha": "^9.1.0", - "omnibridge": "git+https://github.com/peppersec/omnibridge.git#aa3a970c29752a4da5f3fc7ccf0733783c1acf0b", + "omnibridge": "git+https://github.com/peppersec/omnibridge.git#30081f7a735eb03c9d6821a9617cc28efe71a682", "prompt-sync": "^4.2.0", "snarkjs": "git+https://github.com/tornadocash/snarkjs.git#f37f146948f3b28086493e71512006b030588fc2", "tmp-promise": "^3.0.2", diff --git a/scripts/deployBSCHelper.js b/scripts/deployBSCHelper.js deleted file mode 100644 index b3edb5e..0000000 --- a/scripts/deployBSCHelper.js +++ /dev/null @@ -1,21 +0,0 @@ -const { ethers } = require('hardhat') - -// This script deploys L1Helper to FOREIGN chain (mainnet) - -async function main() { - const owner = '0x03Ebd0748Aa4D1457cF479cce56309641e0a98F5' - const omniBridge = '0xf0b456250dc9990662a6f25808cc74a6d1131ea9' - const token = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c' // WBNB - - const Helper = await ethers.getContractFactory('L1Helper') - const helper = await Helper.deploy(omniBridge, token, owner) - await helper.deployed() - console.log(`L1Helper address: ${helper.address}`) -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exit(1) - }) diff --git a/scripts/deployL1Unwrapper.js b/scripts/deployL1Unwrapper.js new file mode 100644 index 0000000..5c466de --- /dev/null +++ b/scripts/deployL1Unwrapper.js @@ -0,0 +1,28 @@ +const { ethers } = require('hardhat') +const config = require('../config') +const { generate } = require('../src/0_generateAddresses') + +// This script deploys L1Helper to FOREIGN chain (mainnet) + +async function deploy({ address, bytecode, singletonFactory }) { + const contractCode = await ethers.provider.getCode(address) + if (contractCode !== '0x') { + console.log(`Contract ${address} already deployed. Skipping...`) + return + } + await singletonFactory.deploy(bytecode, config.salt, { gasLimit: 3000000 }) +} + +async function main() { + const singletonFactory = await ethers.getContractAt('SingletonFactory', config.singletonFactory) + const contracts = await generate() + await deploy({ ...contracts.unwrapperContract, singletonFactory }) + console.log(`L1 unwrapper contract have been deployed on ${contracts.unwrapperContract.address} address`) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/deployTornado.js b/scripts/deployTornado.js index a5d866d..4f1a095 100644 --- a/scripts/deployTornado.js +++ b/scripts/deployTornado.js @@ -1,18 +1,19 @@ const { ethers } = require('hardhat') const { utils } = ethers -const prompt = require('prompt-sync')() +// const prompt = require('prompt-sync')() const MERKLE_TREE_HEIGHT = 23 const { MINIMUM_WITHDRAWAL_AMOUNT, MAXIMUM_DEPOSIT_AMOUNT } = process.env async function main() { require('./compileHasher') - const govAddress = '0x03ebd0748aa4d1457cf479cce56309641e0a98f5' + const govAddress = '0xBAE5aBfa98466Dbe68836763B087f2d189f4D28f' const omniBridge = '0x59447362798334d3485c64D1e4870Fde2DDC0d75' const amb = '0x162e898bd0aacb578c8d5f8d6ca588c13d2a383f' const token = '0xCa8d20f3e0144a72C6B5d576e9Bd3Fd8557E2B04' // WBNB - const l1Unwrapper = '0x2353Dcda746fa1AAD17C5650Ddf2A20112862197' // WBNB -> BNB + const l1Unwrapper = '0x8845F740F8B01bC7D9A4C82a6fD4A60320c07AF1' // WBNB -> BNB const l1ChainId = 56 + const multisig = '0xE3611102E23a43136a13993E3a00BAD67da19119' const Verifier2 = await ethers.getContractFactory('Verifier2') const verifier2 = await Verifier2.deploy() @@ -41,24 +42,28 @@ async function main() { l1Unwrapper, govAddress, l1ChainId, + multisig, ]).slice(1, -1)}\n`, ) - const tornadoImpl = prompt('Deploy tornado pool implementation and provide address here:\n') - // const tornadoImpl = await Pool.deploy( - // verifier2.address, - // verifier16.address, - // MERKLE_TREE_HEIGHT, - // hasher.address, - // token, - // omniBridge, - // l1Unwrapper, - // govAddress, - // ) - // await tornadoImpl.deployed() - // console.log(`TornadoPool implementation address: ${tornadoImpl.address}`) + + //const tornadoImpl = prompt('Deploy tornado pool implementation and provide address here:\n') + const tornadoImpl = await Pool.deploy( + verifier2.address, + verifier16.address, + MERKLE_TREE_HEIGHT, + hasher.address, + token, + omniBridge, + l1Unwrapper, + govAddress, + l1ChainId, + multisig, + ) + await tornadoImpl.deployed() + console.log(`TornadoPool implementation address: ${tornadoImpl.address}`) const CrossChainUpgradeableProxy = await ethers.getContractFactory('CrossChainUpgradeableProxy') - const proxy = await CrossChainUpgradeableProxy.deploy(tornadoImpl, govAddress, [], amb, l1ChainId) + const proxy = await CrossChainUpgradeableProxy.deploy(tornadoImpl.address, govAddress, [], amb, l1ChainId) await proxy.deployed() console.log(`proxy address: ${proxy.address}`) diff --git a/scripts/deployTornadoUpgrade.js b/scripts/deployTornadoUpgrade.js new file mode 100644 index 0000000..b08d4c0 --- /dev/null +++ b/scripts/deployTornadoUpgrade.js @@ -0,0 +1,28 @@ +const { ethers } = require('hardhat') +const config = require('../config') +const { generate } = require('../src/0_generateAddresses') + +// This script deploys Tornado Pool upgrade to L2 (Gnosis Chain) + +async function deploy({ address, bytecode, singletonFactory }) { + const contractCode = await ethers.provider.getCode(address) + if (contractCode !== '0x') { + console.log(`Contract ${address} already deployed. Skipping...`) + return + } + await singletonFactory.deploy(bytecode, config.salt, { gasLimit: 5000000 }) +} + +async function main() { + const singletonFactory = await ethers.getContractAt('SingletonFactory', config.singletonFactory) + const contracts = await generate() + await deploy({ ...contracts.poolContract, singletonFactory }) + console.log(`Upgraded pool contract have been deployed on ${contracts.poolContract.address} address`) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/src/0_generateAddresses.js b/src/0_generateAddresses.js new file mode 100644 index 0000000..27ad3b3 --- /dev/null +++ b/src/0_generateAddresses.js @@ -0,0 +1,68 @@ +const { ethers } = require('hardhat') +const defaultConfig = require('../config') + +async function generate(config = defaultConfig) { + const singletonFactory = await ethers.getContractAt('SingletonFactory', config.singletonFactory) + + const UnwrapperFactory = await ethers.getContractFactory('L1Unwrapper') + const deploymentBytecodeUnwrapper = + UnwrapperFactory.bytecode + + UnwrapperFactory.interface.encodeDeploy([config.omniBridge, config.weth, config.multisig]).slice(2) + + const unwrapperAddress = ethers.utils.getCreate2Address( + singletonFactory.address, + config.salt, + ethers.utils.keccak256(deploymentBytecodeUnwrapper), + ) + + const PoolFactory = await ethers.getContractFactory('TornadoPool') + const deploymentBytecodePool = + PoolFactory.bytecode + + PoolFactory.interface + .encodeDeploy([ + config.verifier2, + config.verifier16, + config.MERKLE_TREE_HEIGHT, + config.hasher, + config.gcWeth, + config.gcOmniBridge, + config.l1Unwrapper, + config.govAddress, + config.l1ChainId, + config.gcMultisig, + ]) + .slice(2) + + const poolAddress = ethers.utils.getCreate2Address( + singletonFactory.address, + config.salt, + ethers.utils.keccak256(deploymentBytecodePool), + ) + + const result = { + unwrapperContract: { + address: unwrapperAddress, + bytecode: deploymentBytecodeUnwrapper, + isProxy: false, + }, + poolContract: { + address: poolAddress, + bytecode: deploymentBytecodePool, + isProxy: false, + }, + } + + return result +} + +async function generateWithLog() { + const contracts = await generate() + console.log('L1 unwrapper contract: ', contracts.unwrapperContract.address) + console.log('Upgraded pool contract: ', contracts.poolContract.address) + return contracts +} + +module.exports = { + generate, + generateWithLog, +} diff --git a/src/index.js b/src/index.js index d99f991..3aec910 100644 --- a/src/index.js +++ b/src/index.js @@ -16,7 +16,17 @@ async function buildMerkleTree({ tornadoPool }) { return new MerkleTree(MERKLE_TREE_HEIGHT, leaves, { hashFunction: poseidonHash2 }) } -async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, relayer, isL1Withdrawal }) { +async function getProof({ + inputs, + outputs, + tree, + extAmount, + fee, + recipient, + relayer, + isL1Withdrawal, + l1Fee, +}) { inputs = shuffle(inputs) outputs = shuffle(outputs) @@ -45,6 +55,7 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela encryptedOutput1: outputs[0].encrypt(), encryptedOutput2: outputs[1].encrypt(), isL1Withdrawal, + l1Fee, } const extDataHash = getExtDataHash(extData) @@ -94,6 +105,7 @@ async function prepareTransaction({ recipient = 0, relayer = 0, isL1Withdrawal = false, + l1Fee = 0, }) { if (inputs.length > 16 || outputs.length > 2) { throw new Error('Incorrect inputs/outputs count') @@ -118,6 +130,7 @@ async function prepareTransaction({ recipient, relayer, isL1Withdrawal, + l1Fee, }) return { diff --git a/src/utils.js b/src/utils.js index 422622f..2ce811b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -22,12 +22,13 @@ function getExtDataHash({ encryptedOutput1, encryptedOutput2, isL1Withdrawal, + l1Fee, }) { const abi = new ethers.utils.AbiCoder() const encodedData = abi.encode( [ - 'tuple(address recipient,int256 extAmount,address relayer,uint256 fee,bytes encryptedOutput1,bytes encryptedOutput2,bool isL1Withdrawal)', + 'tuple(address recipient,int256 extAmount,address relayer,uint256 fee,bytes encryptedOutput1,bytes encryptedOutput2,bool isL1Withdrawal,uint256 l1Fee)', ], [ { @@ -38,6 +39,7 @@ function getExtDataHash({ encryptedOutput1: encryptedOutput1, encryptedOutput2: encryptedOutput2, isL1Withdrawal: isL1Withdrawal, + l1Fee: l1Fee, }, ], ) diff --git a/test/full.test.js b/test/full.test.js index e476e82..28ea23d 100644 --- a/test/full.test.js +++ b/test/full.test.js @@ -9,10 +9,11 @@ const { transaction, registerAndTransact, prepareTransaction, buildMerkleTree } const { toFixedHex, poseidonHash } = require('../src/utils') const { Keypair } = require('../src/keypair') const { encodeDataForBridge } = require('./utils') +const config = require('../config') +const { generate } = require('../src/0_generateAddresses') const MERKLE_TREE_HEIGHT = 5 const l1ChainId = 1 -const MINIMUM_WITHDRAWAL_AMOUNT = utils.parseEther(process.env.MINIMUM_WITHDRAWAL_AMOUNT || '0.05') const MAXIMUM_DEPOSIT_AMOUNT = utils.parseEther(process.env.MAXIMUM_DEPOSIT_AMOUNT || '1') describe('TornadoPool', function () { @@ -26,7 +27,7 @@ describe('TornadoPool', function () { async function fixture() { require('../scripts/compileHasher') - const [sender, gov, l1Unwrapper, multisig] = await ethers.getSigners() + const [sender, gov, multisig] = await ethers.getSigners() const verifier2 = await deploy('Verifier2') const verifier16 = await deploy('Verifier16') const hasher = await deploy('Hasher') @@ -34,9 +35,23 @@ describe('TornadoPool', function () { const token = await deploy('PermittableToken', 'Wrapped ETH', 'WETH', 18, l1ChainId) await token.mint(sender.address, utils.parseEther('10000')) + const l1Token = await deploy('WETH', 'Wrapped ETH', 'WETH') + await l1Token.deposit({ value: utils.parseEther('3') }) + const amb = await deploy('MockAMB', gov.address, l1ChainId) const omniBridge = await deploy('MockOmniBridge', amb.address) + // deploy L1Unwrapper with CREATE2 + const singletonFactory = await ethers.getContractAt('SingletonFactory', config.singletonFactory) + + let customConfig = Object.assign({}, config) + customConfig.omniBridge = omniBridge.address + customConfig.weth = l1Token.address + customConfig.multisig = multisig.address + const contracts = await generate(customConfig) + await singletonFactory.deploy(contracts.unwrapperContract.bytecode, config.salt) + const l1Unwrapper = await ethers.getContractAt('L1Unwrapper', contracts.unwrapperContract.address) + /** @type {TornadoPool} */ const tornadoPoolImpl = await deploy( 'TornadoPool', @@ -52,10 +67,7 @@ describe('TornadoPool', function () { multisig.address, ) - const { data } = await tornadoPoolImpl.populateTransaction.initialize( - MINIMUM_WITHDRAWAL_AMOUNT, - MAXIMUM_DEPOSIT_AMOUNT, - ) + const { data } = await tornadoPoolImpl.populateTransaction.initialize(MAXIMUM_DEPOSIT_AMOUNT) const proxy = await deploy( 'CrossChainUpgradeableProxy', tornadoPoolImpl.address, @@ -69,7 +81,7 @@ describe('TornadoPool', function () { await token.approve(tornadoPool.address, utils.parseEther('10000')) - return { tornadoPool, token, proxy, omniBridge, amb, gov, multisig } + return { tornadoPool, token, proxy, omniBridge, amb, gov, multisig, l1Unwrapper, sender, l1Token } } describe('Upgradeability tests', () => { @@ -88,19 +100,12 @@ describe('TornadoPool', function () { }) it('should configure', async () => { - const { tornadoPool, amb } = await loadFixture(fixture) - const newWithdrawalLimit = utils.parseEther('0.01337') + const { tornadoPool, multisig } = await loadFixture(fixture) const newDepositLimit = utils.parseEther('1337') - const { data } = await tornadoPool.populateTransaction.configureLimits( - newWithdrawalLimit, - newDepositLimit, - ) - - await amb.execute([{ who: tornadoPool.address, callData: data }]) + await tornadoPool.connect(multisig).configureLimits(newDepositLimit) expect(await tornadoPool.maximumDepositAmount()).to.be.equal(newDepositLimit) - expect(await tornadoPool.minimalWithdrawalAmount()).to.be.equal(newWithdrawalLimit) }) }) @@ -271,6 +276,201 @@ describe('TornadoPool', function () { expect(omniBridgeBalance).to.be.equal(aliceWithdrawAmount) }) + it('should withdraw with L1 fee', async function () { + const { tornadoPool, token, omniBridge, l1Unwrapper, sender, l1Token } = await loadFixture(fixture) + const aliceKeypair = new Keypair() // contains private and public keys + + // regular L1 deposit ------------------------------------------- + const aliceDepositAmount = utils.parseEther('0.07') + const aliceDepositUtxo = new Utxo({ amount: aliceDepositAmount, keypair: aliceKeypair }) + const { args, extData } = await prepareTransaction({ + tornadoPool, + outputs: [aliceDepositUtxo], + }) + + let onTokenBridgedData = encodeDataForBridge({ + proof: args, + extData, + }) + + let onTokenBridgedTx = await tornadoPool.populateTransaction.onTokenBridged( + token.address, + aliceDepositUtxo.amount, + onTokenBridgedData, + ) + // emulating bridge. first it sends tokens to omnibridge mock then it sends to the pool + await token.transfer(omniBridge.address, aliceDepositAmount) + let transferTx = await token.populateTransaction.transfer(tornadoPool.address, aliceDepositAmount) + + await omniBridge.execute([ + { who: token.address, callData: transferTx.data }, // send tokens to pool + { who: tornadoPool.address, callData: onTokenBridgedTx.data }, // call onTokenBridgedTx + ]) + + // withdrawal with L1 fee --------------------------------------- + // withdraws a part of his funds from the shielded pool + const aliceWithdrawAmount = utils.parseEther('0.06') + const l1Fee = utils.parseEther('0.01') + // sum of desired withdraw amount and L1 fee are stored in extAmount + const extAmount = aliceWithdrawAmount.add(l1Fee) + const recipient = '0xDeaD00000000000000000000000000000000BEEf' + const aliceChangeUtxo = new Utxo({ + amount: aliceDepositAmount.sub(extAmount), + keypair: aliceKeypair, + }) + await transaction({ + tornadoPool, + inputs: [aliceDepositUtxo], + outputs: [aliceChangeUtxo], + recipient: recipient, + isL1Withdrawal: true, + l1Fee: l1Fee, + }) + + const filter = omniBridge.filters.OnTokenTransfer() + const fromBlock = await ethers.provider.getBlock() + const events = await omniBridge.queryFilter(filter, fromBlock.number) + onTokenBridgedData = events[0].args.data + const hexL1Fee = '0x' + events[0].args.data.toString().slice(66) + expect(ethers.BigNumber.from(hexL1Fee)).to.be.equal(l1Fee) + + const recipientBalance = await token.balanceOf(recipient) + expect(recipientBalance).to.be.equal(0) + const omniBridgeBalance = await token.balanceOf(omniBridge.address) + expect(omniBridgeBalance).to.be.equal(extAmount) + + // L1 transactions: + onTokenBridgedTx = await l1Unwrapper.populateTransaction.onTokenBridged( + l1Token.address, + extAmount, + onTokenBridgedData, + ) + // emulating bridge. first it sends tokens to omniBridge mock then it sends to the recipient + await l1Token.transfer(omniBridge.address, extAmount) + transferTx = await l1Token.populateTransaction.transfer(l1Unwrapper.address, extAmount) + + const senderBalanceBefore = await ethers.provider.getBalance(sender.address) + + let tx = await omniBridge.execute([ + { who: l1Token.address, callData: transferTx.data }, // send tokens to L1Unwrapper + { who: l1Unwrapper.address, callData: onTokenBridgedTx.data }, // call onTokenBridged on L1Unwrapper + ]) + + let receipt = await tx.wait() + let txFee = receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice) + const senderBalanceAfter = await ethers.provider.getBalance(sender.address) + expect(senderBalanceAfter).to.be.equal(senderBalanceBefore.sub(txFee).add(l1Fee)) + expect(await ethers.provider.getBalance(recipient)).to.be.equal(aliceWithdrawAmount) + }) + + it('should set L1FeeReceiver on L1Unwrapper contract', async function () { + const { tornadoPool, token, omniBridge, l1Unwrapper, sender, l1Token, multisig } = await loadFixture( + fixture, + ) + + // check init l1FeeReceiver + expect(await l1Unwrapper.l1FeeReceiver()).to.be.equal(ethers.constants.AddressZero) + + // should not set from not multisig + + await expect(l1Unwrapper.connect(sender).setL1FeeReceiver(multisig.address)).to.be.reverted + + expect(await l1Unwrapper.l1FeeReceiver()).to.be.equal(ethers.constants.AddressZero) + + // should set from multisig + await l1Unwrapper.connect(multisig).setL1FeeReceiver(multisig.address) + + expect(await l1Unwrapper.l1FeeReceiver()).to.be.equal(multisig.address) + + // ------------------------------------------------------------------------ + // check withdraw with L1 fee --------------------------------------------- + + const aliceKeypair = new Keypair() // contains private and public keys + + // regular L1 deposit ------------------------------------------- + const aliceDepositAmount = utils.parseEther('0.07') + const aliceDepositUtxo = new Utxo({ amount: aliceDepositAmount, keypair: aliceKeypair }) + const { args, extData } = await prepareTransaction({ + tornadoPool, + outputs: [aliceDepositUtxo], + }) + + let onTokenBridgedData = encodeDataForBridge({ + proof: args, + extData, + }) + + let onTokenBridgedTx = await tornadoPool.populateTransaction.onTokenBridged( + token.address, + aliceDepositUtxo.amount, + onTokenBridgedData, + ) + // emulating bridge. first it sends tokens to omnibridge mock then it sends to the pool + await token.transfer(omniBridge.address, aliceDepositAmount) + let transferTx = await token.populateTransaction.transfer(tornadoPool.address, aliceDepositAmount) + + await omniBridge.execute([ + { who: token.address, callData: transferTx.data }, // send tokens to pool + { who: tornadoPool.address, callData: onTokenBridgedTx.data }, // call onTokenBridgedTx + ]) + + // withdrawal with L1 fee --------------------------------------- + // withdraws a part of his funds from the shielded pool + const aliceWithdrawAmount = utils.parseEther('0.06') + const l1Fee = utils.parseEther('0.01') + // sum of desired withdraw amount and L1 fee are stored in extAmount + const extAmount = aliceWithdrawAmount.add(l1Fee) + const recipient = '0xDeaD00000000000000000000000000000000BEEf' + const aliceChangeUtxo = new Utxo({ + amount: aliceDepositAmount.sub(extAmount), + keypair: aliceKeypair, + }) + await transaction({ + tornadoPool, + inputs: [aliceDepositUtxo], + outputs: [aliceChangeUtxo], + recipient: recipient, + isL1Withdrawal: true, + l1Fee: l1Fee, + }) + + const filter = omniBridge.filters.OnTokenTransfer() + const fromBlock = await ethers.provider.getBlock() + const events = await omniBridge.queryFilter(filter, fromBlock.number) + onTokenBridgedData = events[0].args.data + const hexL1Fee = '0x' + events[0].args.data.toString().slice(66) + expect(ethers.BigNumber.from(hexL1Fee)).to.be.equal(l1Fee) + + const recipientBalance = await token.balanceOf(recipient) + expect(recipientBalance).to.be.equal(0) + const omniBridgeBalance = await token.balanceOf(omniBridge.address) + expect(omniBridgeBalance).to.be.equal(extAmount) + + // L1 transactions: + onTokenBridgedTx = await l1Unwrapper.populateTransaction.onTokenBridged( + l1Token.address, + extAmount, + onTokenBridgedData, + ) + // emulating bridge. first it sends tokens to omniBridge mock then it sends to the recipient + await l1Token.transfer(omniBridge.address, extAmount) + transferTx = await l1Token.populateTransaction.transfer(l1Unwrapper.address, extAmount) + + const senderBalanceBefore = await ethers.provider.getBalance(sender.address) + const multisigBalanceBefore = await ethers.provider.getBalance(multisig.address) + + let tx = await omniBridge.execute([ + { who: l1Token.address, callData: transferTx.data }, // send tokens to L1Unwrapper + { who: l1Unwrapper.address, callData: onTokenBridgedTx.data }, // call onTokenBridged on L1Unwrapper + ]) + + let receipt = await tx.wait() + let txFee = receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice) + expect(await ethers.provider.getBalance(sender.address)).to.be.equal(senderBalanceBefore.sub(txFee)) + expect(await ethers.provider.getBalance(multisig.address)).to.be.equal(multisigBalanceBefore.add(l1Fee)) + expect(await ethers.provider.getBalance(recipient)).to.be.equal(aliceWithdrawAmount) + }) + it('should transfer funds to multisig in case of L1 deposit fail', async function () { const { tornadoPool, token, omniBridge, multisig } = await loadFixture(fixture) const aliceKeypair = new Keypair() // contains private and public keys diff --git a/test/tree.test.js b/test/tree.test.js index d77263d..2b35b9c 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -71,11 +71,11 @@ describe('MerkleTreeWithHistory', function () { it('should insert', async () => { const { merkleTreeWithHistory } = await loadFixture(fixture) const tree = getNewTree() - merkleTreeWithHistory.insert(toFixedHex(123), toFixedHex(456)) + await merkleTreeWithHistory.insert(toFixedHex(123), toFixedHex(456)) tree.bulkInsert([123, 456]) expect(tree.root()).to.be.be.equal(await merkleTreeWithHistory.getLastRoot()) - merkleTreeWithHistory.insert(toFixedHex(678), toFixedHex(876)) + await merkleTreeWithHistory.insert(toFixedHex(678), toFixedHex(876)) tree.bulkInsert([678, 876]) expect(tree.root()).to.be.be.equal(await merkleTreeWithHistory.getLastRoot()) }) diff --git a/test/utils.js b/test/utils.js index 0d7d194..aa0fa36 100644 --- a/test/utils.js +++ b/test/utils.js @@ -6,7 +6,7 @@ function encodeDataForBridge({ proof, extData }) { return abi.encode( [ 'tuple(bytes proof,bytes32 root,bytes32[] inputNullifiers,bytes32[2] outputCommitments,uint256 publicAmount,bytes32 extDataHash)', - 'tuple(address recipient,int256 extAmount,address relayer,uint256 fee,bytes encryptedOutput1,bytes encryptedOutput2,bool isL1Withdrawal)', + 'tuple(address recipient,int256 extAmount,address relayer,uint256 fee,bytes encryptedOutput1,bytes encryptedOutput2,bool isL1Withdrawal,uint256 l1Fee)', ], [proof, extData], ) diff --git a/yarn.lock b/yarn.lock index 92d6463..000367a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -736,11 +736,6 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-3.4.2.tgz#2c2a1b0fa748235a1f495b6489349776365c51b3" integrity sha512-mDlBS17ymb2wpaLcrqRYdnBAmP1EwqhOXMvqWk2c5Q1N1pm5TkiCtXM9Xzznh4bYsQBq0aIWEkFFE2+iLSN1Tw== -"@openzeppelin/contracts@3.2.2-solc-0.7": - version "3.2.2-solc-0.7" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.2.2-solc-0.7.tgz#8ab169da64438d59f47ca285f1a10efe2f9ba19e" - integrity sha512-vFV53E4pvfsAEjzL9Um2VX9MEuXyq7Hyd9JjnP77AGsrEPxkJaYS06zZIVyhAt3rXTM6QGdW0C282Zv7fM93AA== - "@openzeppelin@git+https://github.com/tornadocash/openzeppelin-contracts.git#6e46aa6946a7f215e7604169ddf46e1aebea850f": version "3.4.1-solc-0.7-2" resolved "git+https://github.com/tornadocash/openzeppelin-contracts.git#6e46aa6946a7f215e7604169ddf46e1aebea850f" @@ -6918,11 +6913,10 @@ oboe@2.1.5: dependencies: http-https "^1.0.0" -"omnibridge@git+https://github.com/peppersec/omnibridge.git#aa3a970c29752a4da5f3fc7ccf0733783c1acf0b": +"omnibridge@git+https://github.com/peppersec/omnibridge.git#30081f7a735eb03c9d6821a9617cc28efe71a682": version "1.1.0" - resolved "git+https://github.com/peppersec/omnibridge.git#aa3a970c29752a4da5f3fc7ccf0733783c1acf0b" + resolved "git+https://github.com/peppersec/omnibridge.git#30081f7a735eb03c9d6821a9617cc28efe71a682" dependencies: - "@openzeppelin/contracts" "3.2.2-solc-0.7" axios "^0.21.0" bignumber.js "^9.0.1" dotenv "^8.2.0"