diff --git a/contracts/bridge/L1Unwrapper.sol b/contracts/bridge/L1Unwrapper.sol index a17da04..77c894f 100644 --- a/contracts/bridge/L1Unwrapper.sol +++ b/contracts/bridge/L1Unwrapper.sol @@ -21,6 +21,13 @@ import { BytesHelper } from "../libraries/Bytes.sol"; 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 { @@ -88,6 +95,22 @@ contract L1Unwrapper is WETHOmnibridgeRouter { uint256 l1Fee = BytesHelper.sliceToUint(_data, 20); AddressHelper.safeSendValue(payable(BytesHelper.bytesToAddress(_data)), _value.sub(l1Fee)); - AddressHelper.safeSendValue(payable(tx.origin), l1Fee); + + address payable l1FeeTo; + if (l1FeeReceiver != payable(address(0))) { + l1FeeTo = l1FeeReceiver; + } else { + l1FeeTo = 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/test/full.test.js b/test/full.test.js index d49b8e5..a4f65d4 100644 --- a/test/full.test.js +++ b/test/full.test.js @@ -48,6 +48,7 @@ describe('TornadoPool', function () { 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) @@ -368,6 +369,114 @@ describe('TornadoPool', function () { 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(42) + 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