From 194258819f7c642c58c244688690c8bb854c62cd Mon Sep 17 00:00:00 2001 From: mpopovac-txfusion <117361243+mpopovac-txfusion@users.noreply.github.com> Date: Fri, 17 Mar 2023 21:55:24 +0100 Subject: [PATCH] WETH Bridge Implementation (#1) Co-authored-by: mpavlovic-txfusion --- ethereum/contracts/bridge/L1WethBridge.sol | 369 ++++++++++++++++++ ethereum/contracts/bridge/WETH9.sol | 87 +++++ .../bridge/interfaces/IL1WethBridge.sol | 53 +++ .../bridge/interfaces/IL2WethBridge.sol | 43 ++ .../contracts/bridge/interfaces/IWETH9.sol | 40 ++ ethereum/package.json | 2 + ethereum/scripts/deploy-weth-bridges.ts | 60 +++ ethereum/scripts/deploy.ts | 1 + ethereum/scripts/initialize-weth-bridges.ts | 182 +++++++++ ethereum/src.ts/deploy.ts | 79 +++- zksync/contracts/bridge/L2Weth.sol | 119 ++++++ zksync/contracts/bridge/L2WethBridge.sol | 118 ++++++ .../contracts/bridge/interfaces/IEthToken.sol | 27 ++ .../bridge/interfaces/IL1WethBridge.sol | 53 +++ .../bridge/interfaces/IL2StandardToken.sol | 2 + .../contracts/bridge/interfaces/IL2Weth.sol | 12 + .../bridge/interfaces/IL2WethBridge.sol | 37 ++ zksync/package.json | 4 +- zksync/yarn.lock | 26 +- 19 files changed, 1298 insertions(+), 16 deletions(-) create mode 100644 ethereum/contracts/bridge/L1WethBridge.sol create mode 100644 ethereum/contracts/bridge/WETH9.sol create mode 100644 ethereum/contracts/bridge/interfaces/IL1WethBridge.sol create mode 100644 ethereum/contracts/bridge/interfaces/IL2WethBridge.sol create mode 100644 ethereum/contracts/bridge/interfaces/IWETH9.sol create mode 100644 ethereum/scripts/deploy-weth-bridges.ts create mode 100644 ethereum/scripts/initialize-weth-bridges.ts create mode 100644 zksync/contracts/bridge/L2Weth.sol create mode 100644 zksync/contracts/bridge/L2WethBridge.sol create mode 100644 zksync/contracts/bridge/interfaces/IEthToken.sol create mode 100644 zksync/contracts/bridge/interfaces/IL1WethBridge.sol create mode 100644 zksync/contracts/bridge/interfaces/IL2Weth.sol create mode 100644 zksync/contracts/bridge/interfaces/IL2WethBridge.sol diff --git a/ethereum/contracts/bridge/L1WethBridge.sol b/ethereum/contracts/bridge/L1WethBridge.sol new file mode 100644 index 000000000..6e26efe41 --- /dev/null +++ b/ethereum/contracts/bridge/L1WethBridge.sol @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "./interfaces/IL1WethBridge.sol"; +import "./interfaces/IL2WethBridge.sol"; +import "./interfaces/IWETH9.sol"; +import "../zksync/interfaces/IMailbox.sol"; +import "../common/interfaces/IAllowList.sol"; + +import "../common/AllowListed.sol"; +import "../common/libraries/UnsafeBytes.sol"; +import "../common/ReentrancyGuard.sol"; +import "../common/L2ContractHelper.sol"; +import "../zksync/Storage.sol"; +import "../zksync/Config.sol"; +import "../vendor/AddressAliasHelper.sol"; + +contract L1WethBridge is IL1WethBridge, AllowListed, ReentrancyGuard { + using SafeERC20 for IERC20; + + /// @dev The address of the WETH token on L1 + address payable public immutable l1WethAddress; + + /// @dev The address of deployed L2 WETH bridge counterpart + address public l2WethBridge; + + /// @dev The smart contract that manages the list with permission to call contract functions + IAllowList immutable allowList; + + /// @dev zkSync smart contract that is used to operate with L2 via asynchronous L2 <-> L1 communication + IMailbox immutable zkSyncMailbox; + + /// @dev The address of the WETH proxy on L2 + address public l2ProxyWethAddress; + + /// @dev The L2 gas limit for requesting L1 -> L2 transaction of deploying L2 bridge instance + /// NOTE: this constant will be accurately calculated in the future. + uint256 constant DEPLOY_L2_BRIDGE_COUNTERPART_GAS_LIMIT = $(PRIORITY_TX_MAX_GAS_LIMIT); + + /// @dev A mapping L2 block number => message number => flag + /// @dev Used to indicate that zkSync L2 -> L1 WETH message was already processed + mapping(uint256 => mapping(uint256 => bool)) public isWithdrawalFinalized; + + /// @dev The accumulated deposited amount per user. + /// @dev A mapping user address => the total deposited WETH amount by the user + mapping(address => uint256) public totalDepositedAmountPerUser; + + /// @dev Contract is expected to be used as proxy implementation. + /// @dev Initialize the implementation to prevent Parity hack. + constructor( + address payable _l1WethAddress, + IMailbox _mailbox, + IAllowList _allowList + ) reentrancyGuardInitializer { + l1WethAddress = _l1WethAddress; + zkSyncMailbox = _mailbox; + allowList = _allowList; + } + + /// @dev Initializes a contract bridge for later use. Expected to be used in the proxy + /// @dev During initialization deploys L2 WETH bridge counterpart as well as provides some factory deps for it + /// @param _factoryDeps A list of raw bytecodes that are needed for deployment of the L2 WETH bridge + /// @notice _factoryDeps[0] == a raw bytecode of L2 WETH bridge implementation + /// @notice _factoryDeps[1] == a raw bytecode of proxy that is used as L2 WETH bridge + /// @param _l2ProxyWethAddress Pre-calculated address of L2 WETH token beacon proxy + /// @param _governor Address which can change L2 WETH token implementation and upgrade the bridge + function initialize( + bytes[] calldata _factoryDeps, + address _l2ProxyWethAddress, + address _governor + ) external reentrancyGuardInitializer { + require(_l2ProxyWethAddress != address(0), "L2 proxy WETH address can not be zero"); + require(_governor != address(0), "Governor address can not be zero"); + require(_factoryDeps.length == 2, "Invalid factory deps length provided"); + + l2ProxyWethAddress = _l2ProxyWethAddress; + + bytes32 l2WethBridgeImplementationBytecodeHash = L2ContractHelper.hashL2Bytecode(_factoryDeps[0]); + bytes32 l2WethBridgeProxyBytecodeHash = L2ContractHelper.hashL2Bytecode(_factoryDeps[1]); + + // Deploy L2 bridge implementation contract + address wethBridgeImplementationAddr = _requestDeployTransaction( + l2WethBridgeImplementationBytecodeHash, + "", // Empty constructor data + _factoryDeps // All factory deps are needed for L2 bridge + ); + + // Prepare the proxy constructor data + bytes memory l2WethBridgeProxyConstructorData; + { + // Data to be used in delegate call to initialize the proxy + bytes memory proxyInitializationParams = abi.encodeCall( + IL2WethBridge.initialize, + (address(this), l1WethAddress, _governor) + ); + l2WethBridgeProxyConstructorData = abi.encode(wethBridgeImplementationAddr, _governor, proxyInitializationParams); + } + + // Deploy L2 bridge proxy contract + l2WethBridge = _requestDeployTransaction( + l2WethBridgeProxyBytecodeHash, + l2WethBridgeProxyConstructorData, + new bytes[](0) // No factory deps are needed for L2 bridge proxy, because it is already passed in previous step + ); + } + + /// @notice Requests L2 transaction that will deploy a contract with a given bytecode hash and constructor data. + /// NOTE: it is always use deploy via create2 with ZERO salt + /// @param _bytecodeHash The hash of the bytecode of the contract to be deployed + /// @param _constructorData The data to be passed to the contract constructor + /// @param _factoryDeps A list of raw bytecodes that are needed for deployment + function _requestDeployTransaction( + bytes32 _bytecodeHash, + bytes memory _constructorData, + bytes[] memory _factoryDeps + ) internal returns (address deployedAddress) { + bytes memory deployCalldata = abi.encodeCall( + IContractDeployer.create2, + (bytes32(0), _bytecodeHash, _constructorData) + ); + zkSyncMailbox.requestL2Transaction( + DEPLOYER_SYSTEM_CONTRACT_ADDRESS, + 0, + deployCalldata, + DEPLOY_L2_BRIDGE_COUNTERPART_GAS_LIMIT, + DEFAULT_L2_GAS_PRICE_PER_PUBDATA, + _factoryDeps, + msg.sender + ); + + deployedAddress = L2ContractHelper.computeCreate2Address( + // Apply the alias to the address of the bridge contract, to get the `msg.sender` in L2. + AddressAliasHelper.applyL1ToL2Alias(address(this)), + bytes32(0), // Zero salt + _bytecodeHash, + keccak256(_constructorData) + ); + } + + function deposit( + address _l2Receiver, + uint256 _amount, + uint256 _l2TxGasLimit, + uint256 _l2TxGasPerPubdataByte + ) external payable nonReentrant returns (bytes32 txHash) { + // ) external payable nonReentrant senderCanCallFunction(allowList) returns (bytes32 txHash) { + require(_amount != 0, "Amount can not be zero"); + require(_l2Receiver != address(0), "L2 receiver address can not be zero"); + + // Deposit WETH tokens from the depositor address to the smart contract address + uint256 depositedAmount = _transferWethFunds(msg.sender, address(this), _amount); + require(depositedAmount == _amount, "Incorrect amount of funds deposited"); + + // // verify the deposit amount is allowed + // _verifyDepositLimit(msg.sender, _amount, false); + + // Unwrap WETH tokens (smart contract address receives the equivalent amount of ETH) + IWETH9(l1WethAddress).withdraw(_amount); + + // Request the finalization of the deposit on the L2 side + bytes memory l2TxCalldata = _getDepositL2Calldata(msg.sender, _l2Receiver, _amount); + + uint256 baseCost = zkSyncMailbox.l2TransactionBaseCost(tx.gasprice, _l2TxGasLimit, _l2TxGasPerPubdataByte); + require(msg.value >= baseCost, "Insufficient ETH value for the base cost"); + + txHash = zkSyncMailbox.requestL2Transaction{value: _amount + msg.value}( + l2WethBridge, + _amount, + l2TxCalldata, + _l2TxGasLimit, + _l2TxGasPerPubdataByte, + new bytes[](0), + msg.sender + ); + + emit DepositInitiated(msg.sender, _l2Receiver, l1WethAddress, _amount); + } + + /// @dev Transfers WETH tokens from the depositor to the receiver address + /// @return The difference between the receiver balance before and after the transferring funds + function _transferWethFunds( + address _from, + address _to, + uint256 _amount + ) internal returns (uint256) { + IWETH9 l1Weth = IWETH9(l1WethAddress); + + uint256 balanceBefore = l1Weth.balanceOf(_to); + l1Weth.transferFrom(_from, _to, _amount); + uint256 balanceAfter = l1Weth.balanceOf(_to); + + return balanceAfter - balanceBefore; + } + + /// @dev Verify the WETH deposit limit is reached to its cap or not + function _verifyDepositLimit( + address _depositor, + uint256 _amount, + bool _claiming + ) internal { + IAllowList.Deposit memory limitData = IAllowList(allowList).getTokenDepositLimitData(l1WethAddress); + if (!limitData.depositLimitation) return; // no deposit limitation is placed for this token + + if (_claiming) { + totalDepositedAmountPerUser[_depositor] -= _amount; + } else { + require(totalDepositedAmountPerUser[_depositor] + _amount <= limitData.depositCap, "Deposit cap reached"); + totalDepositedAmountPerUser[_depositor] += _amount; + } + } + + /// @dev Generate a calldata for calling the deposit finalization on the L2 WETH bridge contract + function _getDepositL2Calldata( + address _l1Sender, + address _l2Receiver, + uint256 _amount + ) internal pure returns (bytes memory txCalldata) { + txCalldata = abi.encodeCall( + IL2WethBridge.finalizeDeposit, + (_l1Sender, _l2Receiver, _amount) + ); + } + + /// @notice Withdraw funds from the initiated deposit, that failed when finalizing on L2. + /// Note: Refund is performed by sending equivalent amount of ETH to refund recipient address on L2. + function claimFailedDeposit( + address, // _depositSender, + bytes32, // _l2TxHash + uint256, // _l2BlockNumber, + uint256, // _l2MessageIndex, + uint16, // _l2TxNumberInBlock, + bytes32[] calldata // _merkleProof + ) external nonReentrant { + // ) external nonReentrant senderCanCallFunction(allowList) { + revert( + "Method not supported. ETH refund is handled by the zkSync contract." + ); + } + + /// @notice Finalize the WETH withdrawal and release funds + /// @param _l2BlockNumber The L2 block number where the WETH withdrawal was processed + /// @param _l2MessageIndexes The position in the L2 logs Merkle tree of the l2Logs that were sent with the ETH and WETH withdrawal messages + /// @param _l2TxNumberInBlock The L2 transaction number in a block, in which the ETH and WETH withdrawal logs were sent + /// @param _messages The L2 ETH and WETH withdraw data, stored in an L2 -> L1 messages + /// @param _merkleProofs The Merkle proofs of the inclusion L2 -> L1 messages about ETH and WETH withdrawal initializations + function finalizeWithdrawal( + uint256 _l2BlockNumber, + FinalizeWithdrawalL2MessageIndexes calldata _l2MessageIndexes, + uint16 _l2TxNumberInBlock, + FinalizeWithdrawalMessages calldata _messages, + FinalizeWithdrawalMerkleProofs calldata _merkleProofs + ) external nonReentrant { + // ) external nonReentrant senderCanCallFunction(allowList) { + require(!isWithdrawalFinalized[_l2BlockNumber][_l2MessageIndexes.wethL2MessageIndex], "WETH withdrawal is already finalized"); + + _proveL2MessagesInclusion( + _l2BlockNumber, + _l2MessageIndexes, + _l2TxNumberInBlock, + _messages, + _merkleProofs + ); + + (address _l1EthWithdrawReceiver, uint256 _ethAmount) = _parseL2EthWithdrawalMessage(_messages.ethMessage); + require(_l1EthWithdrawReceiver == address(this), "Wrong L1 ETH withdraw receiver"); + + (address _l1WethWithdrawReceiver, uint256 _wethAmount) = _parseL2WethWithdrawalMessage(_messages.wethMessage); + require(_l1WethWithdrawReceiver != address(0), "L1 WETH withdraw receiver can not be zero"); + + require(_ethAmount == _wethAmount, "Unequal ETH and WETH amounts in the L2 messages"); + + // Widthdraw ETH to smart contract address + zkSyncMailbox.finalizeEthWithdrawal( + _l2BlockNumber, + _l2MessageIndexes.ethL2MessageIndex, + _l2TxNumberInBlock, + _messages.ethMessage, + _merkleProofs.ethProof + ); + + // Wrap ETH to WETH tokens (smart contract address receives the equivalent amount of WETH) + IWETH9(l1WethAddress).deposit{value: _ethAmount}(); + + // Transfer WETH tokens from the smart contract address to the withdrawal receiver + uint256 withdrawnAmount = _transferWethFunds(address(this), _l1WethWithdrawReceiver, _wethAmount); + require(withdrawnAmount == _wethAmount, "Incorrect amount of funds withdrawn"); + + isWithdrawalFinalized[_l2BlockNumber][_l2MessageIndexes.wethL2MessageIndex] = true; + + emit WithdrawalFinalized(_l1WethWithdrawReceiver, l1WethAddress, _wethAmount); + } + + function _proveL2MessagesInclusion( + uint256 _l2BlockNumber, + FinalizeWithdrawalL2MessageIndexes calldata _l2MessageIndexes, + uint16 _l2TxNumberInBlock, + FinalizeWithdrawalMessages calldata _messages, + FinalizeWithdrawalMerkleProofs calldata _merkleProofs + ) internal view { + L2Message memory l2ToL1EthMessage = L2Message({ + txNumberInBlock: _l2TxNumberInBlock, + sender: L2_ETH_TOKEN_ADDRESS, + data: _messages.ethMessage + }); + + bool ethMessageProofValid = zkSyncMailbox.proveL2MessageInclusion( + _l2BlockNumber, + _l2MessageIndexes.ethL2MessageIndex, + l2ToL1EthMessage, + _merkleProofs.ethProof + ); + require(ethMessageProofValid, "ETH L2 message inclusion proof is invalid"); + + L2Message memory l2ToL1WethMessage = L2Message({ + txNumberInBlock: _l2TxNumberInBlock, + sender: l2WethBridge, + data: _messages.wethMessage + }); + + bool wethMessageProofValid = zkSyncMailbox.proveL2MessageInclusion( + _l2BlockNumber, + _l2MessageIndexes.wethL2MessageIndex, + l2ToL1WethMessage, + _merkleProofs.wethProof + ); + require(wethMessageProofValid, "WETH L2 message inclusion proof is invalid"); + } + + /// @dev Decode the ETH withdraw message that came from L2EthToken contract + function _parseL2EthWithdrawalMessage(bytes memory _message) + internal + pure + returns (address l1Receiver, uint256 amount) + { + // Check that the message length is correct. + // It should be equal to the length of the function signature + address + uint256 = 4 + 20 + 32 = 56 (bytes). + require(_message.length == 56, "Incorrect ETH message length"); + + (uint32 functionSignature, uint256 offset) = UnsafeBytes.readUint32(_message, 0); + require(bytes4(functionSignature) == IMailbox.finalizeEthWithdrawal.selector); + + (l1Receiver, offset) = UnsafeBytes.readAddress(_message, offset); + (amount, offset) = UnsafeBytes.readUint256(_message, offset); + } + + /// @dev Decode the WETH withdraw message that came from L2WethBridge contract + function _parseL2WethWithdrawalMessage(bytes memory _message) + internal + pure + returns (address l1WethReceiver, uint256 amount) + { + // Check that the message length is correct. + // It should be equal to the length of the function signature + L2 sender address + L1 receiver address + uint256 = 4 + 20 + 20 + 32 = 76 (bytes). + require(_message.length == 76, "Incorrect WETH message length"); + + (uint32 functionSignature, uint256 offset) = UnsafeBytes.readUint32(_message, 0); + require(bytes4(functionSignature) == this.finalizeWithdrawal.selector, "Incorrect WETH message function selector"); + + address l2WethSender; + (l2WethSender, offset) = UnsafeBytes.readAddress(_message, offset); + (l1WethReceiver, offset) = UnsafeBytes.readAddress(_message, offset); + (amount, offset) = UnsafeBytes.readUint256(_message, offset); + } + + receive() external payable {} +} \ No newline at end of file diff --git a/ethereum/contracts/bridge/WETH9.sol b/ethereum/contracts/bridge/WETH9.sol new file mode 100644 index 000000000..bb51e0cd9 --- /dev/null +++ b/ethereum/contracts/bridge/WETH9.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { IWETH9 } from "./interfaces/IWETH9.sol"; + +contract WETH9 is IWETH9 { + string public name; + string public symbol; + uint8 public decimals; + + mapping(address => uint) public override balanceOf; + mapping(address => mapping(address => uint)) public override allowance; + + constructor() public { + name = "Wrapped Ether"; + symbol = "WETH"; + decimals = 18; + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint value) external override { + balanceOf[msg.sender] -= value; + (bool success, ) = msg.sender.call{value: value}(""); + if (!success) { + revert WETH_ETHTransferFailed(); + } + emit Withdrawal(msg.sender, value); + } + + function totalSupply() external view override returns (uint) { + return address(this).balance; + } + + function approve( + address spender, + uint value + ) external override returns (bool) { + allowance[msg.sender][spender] = value; + emit Approval(msg.sender, spender, value); + return true; + } + + function transfer( + address to, + uint value + ) external override ensuresRecipient(to) returns (bool) { + balanceOf[msg.sender] -= value; + balanceOf[to] += value; + + emit Transfer(msg.sender, to, value); + return true; + } + + function transferFrom( + address from, + address to, + uint value + ) external override ensuresRecipient(to) returns (bool) { + if (from != msg.sender) { + uint _allowance = allowance[from][msg.sender]; + if (_allowance != type(uint).max) { + allowance[from][msg.sender] -= value; + } + } + + balanceOf[from] -= value; + balanceOf[to] += value; + + emit Transfer(from, to, value); + return true; + } + + modifier ensuresRecipient(address to) { + // Prevents from burning or sending WETH tokens to the contract. + if (to == address(0)) { + revert WETH_InvalidTransferRecipient(); + } + if (to == address(this)) { + revert WETH_InvalidTransferRecipient(); + } + _; + } +} diff --git a/ethereum/contracts/bridge/interfaces/IL1WethBridge.sol b/ethereum/contracts/bridge/interfaces/IL1WethBridge.sol new file mode 100644 index 000000000..198d14027 --- /dev/null +++ b/ethereum/contracts/bridge/interfaces/IL1WethBridge.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @author Matter Labs +interface IL1WethBridge { + struct FinalizeWithdrawalL2MessageIndexes { + uint256 ethL2MessageIndex; + uint256 wethL2MessageIndex; + } + + struct FinalizeWithdrawalMessages { + bytes ethMessage; + bytes wethMessage; + } + + struct FinalizeWithdrawalMerkleProofs { + bytes32[] ethProof; + bytes32[] wethProof; + } + + event DepositInitiated(address indexed from, address indexed to, address indexed l1Token, uint256 amount); + + event WithdrawalFinalized(address indexed to, address indexed l1Token, uint256 amount); + + event ClaimedFailedDeposit(address indexed to, address indexed l1Token, uint256 amount); + + function isWithdrawalFinalized(uint256 _l2BlockNumber, uint256 _l2MessageIndex) external view returns (bool); + + function deposit( + address _l2Receiver, + uint256 _amount, + uint256 _l2TxGasLimit, + uint256 _l2TxGasPerPubdataByte + ) external payable returns (bytes32 txHash); + + function claimFailedDeposit( + address _depositSender, + bytes32 _l2TxHash, + uint256 _l2BlockNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBlock, + bytes32[] calldata _merkleProof + ) external; + + function finalizeWithdrawal( + uint256 _l2BlockNumber, + FinalizeWithdrawalL2MessageIndexes calldata _l2MessageIndexes, + uint16 _l2TxNumberInBlock, + FinalizeWithdrawalMessages calldata _messages, + FinalizeWithdrawalMerkleProofs calldata _merkleProofs + ) external; +} diff --git a/ethereum/contracts/bridge/interfaces/IL2WethBridge.sol b/ethereum/contracts/bridge/interfaces/IL2WethBridge.sol new file mode 100644 index 000000000..4e4a039be --- /dev/null +++ b/ethereum/contracts/bridge/interfaces/IL2WethBridge.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @author Matter Labs +interface IL2WethBridge { + function initialize( + address _l1Bridge, + address _l1WethAddress, + address _governor + ) external; + + event FinalizeDeposit( + address indexed l1Sender, + address indexed l2Receiver, + address indexed l2Weth, + uint256 amount + ); + + event WithdrawalInitiated( + address indexed l2Sender, + address indexed l1Receiver, + address indexed l2Weth, + uint256 amount + ); + + function finalizeDeposit( + address _l1Sender, + address _l2Receiver, + uint256 _amount + ) external payable; + + function withdraw( + address _l1Receiver, + uint256 _amount + ) external; + + function l1WethAddress() external view returns (address); + + function l2WethAddress() external view returns (address); + + function l1WethBridge() external view returns (address); +} diff --git a/ethereum/contracts/bridge/interfaces/IWETH9.sol b/ethereum/contracts/bridge/interfaces/IWETH9.sol new file mode 100644 index 000000000..fd41912ed --- /dev/null +++ b/ethereum/contracts/bridge/interfaces/IWETH9.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +interface IWETH9 { + function deposit() external payable; + + function withdraw(uint wad) external; + + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + error WETH_ETHTransferFailed(); + error WETH_InvalidTransferRecipient(); + + // ERC20 + // function name() external view returns (string memory); + + // function symbol() external view returns (string memory); + + // function decimals() external view returns (uint8); + + function totalSupply() external view returns (uint); + + function balanceOf(address guy) external view returns (uint); + + function allowance(address src, address dst) external view returns (uint); + + function approve(address spender, uint wad) external returns (bool); + + function transfer(address dst, uint wad) external returns (bool); + + function transferFrom( + address src, + address dst, + uint wad + ) external returns (bool); + + event Approval(address indexed src, address indexed dst, uint wad); + event Transfer(address indexed src, address indexed dst, uint wad); +} diff --git a/ethereum/package.json b/ethereum/package.json index 90b514e3a..6fa42a96f 100644 --- a/ethereum/package.json +++ b/ethereum/package.json @@ -49,6 +49,8 @@ "test": "CONTRACT_TESTS=1 yarn run hardhat test test/unit_tests/*.spec.ts --network hardhat", "test:fork": "CONTRACT_TESTS=1 TEST_CONTRACTS_FORK=1 yarn run hardhat test test/unit_tests/*.fork.ts --network hardhat", "deploy-no-build": "ts-node scripts/deploy.ts", + "deploy-weth-bridges": "ts-node scripts/deploy-weth-bridges.ts", + "initialize-weth-bridges": "ts-node scripts/initialize-weth-bridges.ts", "allow-list-manager": "ts-node scripts/allow-list-manager.ts", "deploy-erc20": "ts-node scripts/deploy-erc20.ts", "token-info": "ts-node scripts/token-info.ts", diff --git a/ethereum/scripts/deploy-weth-bridges.ts b/ethereum/scripts/deploy-weth-bridges.ts new file mode 100644 index 000000000..8b97cfc04 --- /dev/null +++ b/ethereum/scripts/deploy-weth-bridges.ts @@ -0,0 +1,60 @@ +import { Command } from 'commander'; +import { Wallet, ethers } from 'ethers'; +import { Deployer } from '../src.ts/deploy'; +import { formatUnits, parseUnits } from 'ethers/lib/utils'; +import * as fs from 'fs'; +import * as path from 'path'; +import { web3Provider } from './utils'; + +const provider = web3Provider(); +const testConfigPath = path.join(process.env.ZKSYNC_HOME as string, `etc/test_config/constant`); +const ethTestConfig = JSON.parse(fs.readFileSync(`${testConfigPath}/eth.json`, { encoding: 'utf-8' })); + +async function main() { + const program = new Command(); + + program.version('0.1.0').name('deploy').description('deploy weth bridges'); + + program + .option('--private-key ') + .option('--gas-price ') + .option('--nonce ') + .option('--governor-address ') + .option('--create2-salt ') + .action(async (cmd) => { + const deployWallet = cmd.privateKey + ? new Wallet(cmd.privateKey, provider) + : Wallet.fromMnemonic( + process.env.MNEMONIC ? process.env.MNEMONIC : ethTestConfig.mnemonic, + "m/44'/60'/0'/0/0" + ).connect(provider); + console.log(`Using deployer wallet: ${deployWallet.address}`); + + const governorAddress = cmd.governorAddress ? cmd.governorAddress : deployWallet.address; + console.log(`Using governor address: ${governorAddress}`); + + const gasPrice = cmd.gasPrice ? parseUnits(cmd.gasPrice, 'gwei') : await provider.getGasPrice(); + console.log(`Using gas price: ${formatUnits(gasPrice, 'gwei')} gwei`); + + let nonce = cmd.nonce ? parseInt(cmd.nonce) : await deployWallet.getTransactionCount(); + console.log(`Using nonce: ${nonce}`); + + const create2Salt = cmd.create2Salt ? cmd.create2Salt : ethers.utils.hexlify(ethers.utils.randomBytes(32)); + + const deployer = new Deployer({ + deployWallet, + governorAddress, + verbose: true + }); + + await deployer.deployWethBridgeContracts(create2Salt, gasPrice); + }) + await program.parseAsync(process.argv); +} + +main() + .then(() => process.exit(0)) + .catch((err) => { + console.error('Error:', err); + process.exit(1); + }); \ No newline at end of file diff --git a/ethereum/scripts/deploy.ts b/ethereum/scripts/deploy.ts index fe5ea3386..737bcf9a5 100644 --- a/ethereum/scripts/deploy.ts +++ b/ethereum/scripts/deploy.ts @@ -67,6 +67,7 @@ async function main() { await deployer.deployAllowList(create2Salt, { gasPrice, nonce }); await deployer.deployZkSyncContract(create2Salt, gasPrice, nonce + 1); await deployer.deployBridgeContracts(create2Salt, gasPrice); // Do not pass nonce, since it was increment after deploying zkSync contracts + // await deployer.deployWethBridgeContracts(create2Salt, gasPrice); }); await program.parseAsync(process.argv); diff --git a/ethereum/scripts/initialize-weth-bridges.ts b/ethereum/scripts/initialize-weth-bridges.ts new file mode 100644 index 000000000..60d938b47 --- /dev/null +++ b/ethereum/scripts/initialize-weth-bridges.ts @@ -0,0 +1,182 @@ +import { Command } from 'commander'; +import { ethers, Wallet } from 'ethers'; +import { Deployer } from '../src.ts/deploy'; +import { formatUnits, parseUnits } from 'ethers/lib/utils'; +import { + computeL2Create2Address, + web3Provider, + hashL2Bytecode, + applyL1ToL2Alias, + getNumberFromEnv, + DEFAULT_L2_GAS_PRICE_PER_PUBDATA +} from './utils'; + +import * as fs from 'fs'; +import * as path from 'path'; + +const provider = web3Provider(); +const testConfigPath = path.join(process.env.ZKSYNC_HOME as string, `etc/test_config/constant`); +const ethTestConfig = JSON.parse(fs.readFileSync(`${testConfigPath}/eth.json`, { encoding: 'utf-8' })); + +const contractArtifactsPath = path.join(process.env.ZKSYNC_HOME as string, 'contracts/zksync/artifacts-zk/'); + +const l2BridgeArtifactsPath = path.join(contractArtifactsPath, 'cache-zk/solpp-generated-contracts/bridge/'); + +const openzeppelinTransparentProxyArtifactsPath = path.join( + contractArtifactsPath, + '@openzeppelin/contracts/proxy/transparent/' +); + +function readBytecode(path: string, fileName: string) { + return JSON.parse(fs.readFileSync(`${path}/${fileName}.sol/${fileName}.json`, { encoding: 'utf-8' })).bytecode; +} + +function readInterface(path: string, fileName: string) { + const abi = JSON.parse(fs.readFileSync(`${path}/${fileName}.sol/${fileName}.json`, { encoding: 'utf-8' })).abi; + return new ethers.utils.Interface(abi); +} + +const L2_WETH_BRIDGE_PROXY_BYTECODE = readBytecode(openzeppelinTransparentProxyArtifactsPath, 'TransparentUpgradeableProxy'); +const L2_WETH_BRIDGE_IMPLEMENTATION_BYTECODE = readBytecode(l2BridgeArtifactsPath, 'L2WethBridge'); +const L2_WETH_PROXY_BYTECODE = readBytecode(openzeppelinTransparentProxyArtifactsPath, 'TransparentUpgradeableProxy'); +const L2_WETH_IMPLEMENTATION_BYTECODE = readBytecode(l2BridgeArtifactsPath, 'L2Weth'); + +const L2_WETH_BRIDGE_INTERFACE = readInterface(l2BridgeArtifactsPath, 'L2WethBridge'); +const L2_WETH_INTERFACE = readInterface(l2BridgeArtifactsPath, 'L2Weth'); + +async function main() { + const program = new Command(); + + program.version('0.1.0').name('initialize-weth-bridges'); + + program + .option('--deployer-private-key ') + .requiredOption('--initializer-private-key ') + .option('--gas-price ') + .option('--l1-weth-address ') + .option('--nonce ') + .action(async (cmd) => { + const deployWallet = cmd.deployerPrivateKey + ? new Wallet(cmd.deployerPrivateKey, provider) + : Wallet.fromMnemonic( + process.env.MNEMONIC ? process.env.MNEMONIC : ethTestConfig.mnemonic, + "m/44'/60'/0'/0/0" + ).connect(provider); + console.log(`Using deployer wallet: ${deployWallet.address}`); + + const gasPrice = cmd.gasPrice ? parseUnits(cmd.gasPrice, 'gwei') : await provider.getGasPrice(); + console.log(`Using gas price: ${formatUnits(gasPrice, 'gwei')} gwei`); + + const nonce = cmd.nonce ? parseInt(cmd.nonce) : await deployWallet.getTransactionCount(); + console.log(`Using deployer nonce: ${nonce}`); + + const l1WethAddress = cmd.l1WethAddress || process.env.CONTRACTS_L1_WETH_TOKEN_ADDR; + + const deployer = new Deployer({ + deployWallet, + governorAddress: deployWallet.address, + verbose: true + }); + + const zkSync = deployer.zkSyncContract(deployWallet); + + const initializerWallet = new Wallet(cmd.initializerPrivateKey, provider) + console.log(`Using initializer wallet: ${initializerWallet.address}`); + const initializerNonce = await initializerWallet.getTransactionCount(); + console.log(`Using initializer nonce: ${initializerNonce}`); + const wethBridge = deployer.defaultWethBridge(initializerWallet); + + const priorityTxMaxGasLimit = getNumberFromEnv('CONTRACTS_PRIORITY_TX_MAX_GAS_LIMIT'); + const governorAddress = await zkSync.getGovernor(); + const abiCoder = new ethers.utils.AbiCoder(); + + const l2WethBridgeImplAddr = computeL2Create2Address( + applyL1ToL2Alias(wethBridge.address), + L2_WETH_BRIDGE_IMPLEMENTATION_BYTECODE, + '0x', + ethers.constants.HashZero + ); + + const l2WethBridgeProxyInitializationParams = L2_WETH_BRIDGE_INTERFACE.encodeFunctionData('initialize', [ + wethBridge.address, + l1WethAddress, + governorAddress + ]); + const l2WethBridgeProxyAddr = computeL2Create2Address( + applyL1ToL2Alias(wethBridge.address), + L2_WETH_BRIDGE_PROXY_BYTECODE, + ethers.utils.arrayify( + abiCoder.encode( + ['address', 'address', 'bytes'], + [l2WethBridgeImplAddr, governorAddress, l2WethBridgeProxyInitializationParams] + ) + ), + ethers.constants.HashZero + ); + + const l2WethAddr = computeL2Create2Address( + l2WethBridgeProxyAddr, + L2_WETH_IMPLEMENTATION_BYTECODE, + '0x', + ethers.constants.HashZero + ); + + const l2WethProxyInitializationParams = L2_WETH_INTERFACE.encodeFunctionData('bridgeInitialize', [ + l2WethBridgeImplAddr, + deployer.addresses.WethToken, + "Wrapped Ether", + "WETH" + ]); + const l2WethProxyAddr = computeL2Create2Address( + l2WethBridgeProxyAddr, + L2_WETH_PROXY_BYTECODE, + ethers.utils.arrayify( + abiCoder.encode( + ['address', 'address', 'bytes'], + [l2WethAddr, governorAddress, l2WethProxyInitializationParams] + ) + ), + ethers.constants.HashZero + ); + + const independentInitialization = [ + zkSync.requestL2Transaction( + ethers.constants.AddressZero, + 0, + '0x', + priorityTxMaxGasLimit, + DEFAULT_L2_GAS_PRICE_PER_PUBDATA, + [L2_WETH_PROXY_BYTECODE, L2_WETH_IMPLEMENTATION_BYTECODE], + deployWallet.address, + { gasPrice, nonce } + ), + wethBridge.initialize( + [ + L2_WETH_BRIDGE_IMPLEMENTATION_BYTECODE, + L2_WETH_BRIDGE_PROXY_BYTECODE + ], + l2WethProxyAddr, + governorAddress, + { + gasPrice, + nonce: initializerNonce + } + ) + ]; + + const txs = await Promise.all(independentInitialization); + const receipts = await Promise.all(txs.map((tx) => tx.wait())); + + console.log(`WETH bridge initialized, gasUsed: ${receipts[1].gasUsed.toString()}`); + console.log(`CONTRACTS_L2_WETH_BRIDGE_ADDR=${await wethBridge.l2WethBridge()}`); + }); + + await program.parseAsync(process.argv); +} + +main() + .then(() => process.exit(0)) + .catch((err) => { + console.error('Error:', err); + process.exit(1); + }); \ No newline at end of file diff --git a/ethereum/src.ts/deploy.ts b/ethereum/src.ts/deploy.ts index 31d09b724..7ed7e3f16 100644 --- a/ethereum/src.ts/deploy.ts +++ b/ethereum/src.ts/deploy.ts @@ -6,6 +6,7 @@ import { Interface } from 'ethers/lib/utils'; import { Action, facetCut, diamondCut } from './diamondCut'; import { IZkSyncFactory } from '../typechain/IZkSyncFactory'; import { L1ERC20BridgeFactory } from '../typechain/L1ERC20BridgeFactory'; +import { L1WethBridgeFactory } from '../typechain/L1WethBridgeFactory'; import { SingletonFactoryFactory } from '../typechain/SingletonFactoryFactory'; import { AllowListFactory } from '../typechain'; import { hexlify } from 'ethers/lib/utils'; @@ -36,9 +37,12 @@ export interface DeployedAddresses { Bridges: { ERC20BridgeImplementation: string; ERC20BridgeProxy: string; + WethBridgeImplementation: string; + WethBridgeProxy: string; }; AllowList: string; Create2Factory: string; + WethToken: string; } export interface DeployerConfig { @@ -62,10 +66,13 @@ export function deployedAddressesFromEnv(): DeployedAddresses { }, Bridges: { ERC20BridgeImplementation: getAddressFromEnv('CONTRACTS_L1_ERC20_BRIDGE_IMPL_ADDR'), - ERC20BridgeProxy: getAddressFromEnv('CONTRACTS_L1_ERC20_BRIDGE_PROXY_ADDR') + ERC20BridgeProxy: getAddressFromEnv('CONTRACTS_L1_ERC20_BRIDGE_PROXY_ADDR'), + WethBridgeImplementation: getAddressFromEnv('CONTRACTS_L1_WETH_BRIDGE_IMPL_ADDR'), + WethBridgeProxy: getAddressFromEnv('CONTRACTS_L1_WETH_BRIDGE_PROXY_ADDR'), }, AllowList: getAddressFromEnv('CONTRACTS_L1_ALLOW_LIST_ADDR'), - Create2Factory: getAddressFromEnv('CONTRACTS_CREATE2_FACTORY_ADDR') + Create2Factory: getAddressFromEnv('CONTRACTS_CREATE2_FACTORY_ADDR'), + WethToken: getAddressFromEnv('CONTRACTS_L1_WETH_TOKEN_ADDR') }; } @@ -324,6 +331,60 @@ export class Deployer { this.addresses.Bridges.ERC20BridgeProxy = contractAddress; } + public async deployWethToken( + create2Salt: string, + ethTxOptions: ethers.providers.TransactionRequest + ) { + ethTxOptions.gasLimit ??= 10_000_000; + const contractAddress = await this.deployViaCreate2( + 'WETH9', + [], + create2Salt, + ethTxOptions + ); + + if (this.verbose) { + console.log(`CONTRACTS_L1_WETH_TOKEN_ADDR=${contractAddress}`); + } + + this.addresses.WethToken = contractAddress; + } + + public async deployWethBridgeImplementation( + create2Salt: string, + ethTxOptions: ethers.providers.TransactionRequest + ) { + ethTxOptions.gasLimit ??= 10_000_000; + const contractAddress = await this.deployViaCreate2( + 'L1WethBridge', + [this.addresses.WethToken, this.addresses.ZkSync.DiamondProxy, this.addresses.AllowList], + create2Salt, + ethTxOptions + ); + + if (this.verbose) { + console.log(`CONTRACTS_L1_WETH_BRIDGE_IMPL_ADDR=${contractAddress}`); + } + + this.addresses.Bridges.WethBridgeImplementation = contractAddress; + } + + public async deployWethBridgeProxy(create2Salt: string, ethTxOptions: ethers.providers.TransactionRequest) { + ethTxOptions.gasLimit ??= 10_000_000; + const contractAddress = await this.deployViaCreate2( + 'TransparentUpgradeableProxy', + [this.addresses.Bridges.WethBridgeImplementation, this.governorAddress, '0x'], + create2Salt, + ethTxOptions + ); + + if (this.verbose) { + console.log(`CONTRACTS_L1_WETH_BRIDGE_PROXY_ADDR=${contractAddress}`); + } + + this.addresses.Bridges.WethBridgeProxy = contractAddress; + } + public async deployDiamondInit(create2Salt: string, ethTxOptions: ethers.providers.TransactionRequest) { ethTxOptions.gasLimit ??= 10_000_000; const contractAddress = await this.deployViaCreate2('DiamondInit', [], create2Salt, ethTxOptions); @@ -400,6 +461,16 @@ export class Deployer { await this.deployERC20BridgeProxy(create2Salt, { gasPrice, nonce: nonce + 1 }); } + public async deployWethBridgeContracts(create2Salt: string, gasPrice?: BigNumberish, nonce?) { + nonce = nonce ? parseInt(nonce) : await this.deployWallet.getTransactionCount(); + + if (process.env.CHAIN_ETH_NETWORK === 'localhost') { + await this.deployWethToken(create2Salt, { gasPrice, nonce: nonce++ }); + } + await this.deployWethBridgeImplementation(create2Salt, { gasPrice, nonce: nonce++ }); + await this.deployWethBridgeProxy(create2Salt, { gasPrice, nonce: nonce++ }); + } + public create2FactoryContract(signerOrProvider: Signer | providers.Provider) { return SingletonFactoryFactory.connect(this.addresses.Create2Factory, signerOrProvider); } @@ -415,4 +486,8 @@ export class Deployer { public defaultERC20Bridge(signerOrProvider: Signer | providers.Provider) { return L1ERC20BridgeFactory.connect(this.addresses.Bridges.ERC20BridgeProxy, signerOrProvider); } + + public defaultWethBridge(signerOrProvider: Signer | providers.Provider) { + return L1WethBridgeFactory.connect(this.addresses.Bridges.WethBridgeProxy, signerOrProvider); + } } diff --git a/zksync/contracts/bridge/L2Weth.sol b/zksync/contracts/bridge/L2Weth.sol new file mode 100644 index 000000000..0c172fcec --- /dev/null +++ b/zksync/contracts/bridge/L2Weth.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; + +import "./interfaces/IL2Weth.sol"; +import "./interfaces/IL2StandardToken.sol"; + +/// @author Matter Labs +/// @notice The canonical implementation of the WETH token. +/// @dev The idea is to replace the legacy WETH9 (which has well-known issues) with something better. +/// This implementation has the following differences from the WETH9: +/// - It does not have a silent fallback method and will revert if it's called for a method it hasn't implemented. +/// - It implements `receive` method to allow users to deposit ether directly. +/// - It implements `permit` method to allow users to sign a message instead of calling `approve`. +/// +/// Note: This is an upgradeable contract. In the future, we will remove upgradeability to make it trustless. +/// But for now, when the Rollup has instant upgradability, we leave the possibility of upgrading to improve the contract if needed. +contract L2Weth is ERC20PermitUpgradeable, IL2Weth, IL2StandardToken { + /// @dev Address of the L2 WETH Bridge. + address public override l2Bridge; + + /// @dev Address of the L1 WETH token. It can be deposited to mint this L2 token. + address public override l1Address; + + /// @dev Contract is expected to be used as proxy implementation. + constructor() { + // Disable initialization to prevent Parity hack. + _disableInitializers(); + } + + /// @notice Initializes a contract token for later use. Expected to be used in the proxy. + /// @dev Stores the L1 address of the bridge and set `name`/`symbol`/`decimals` getters. + /// @param _l2Bridge Address of the L2 bridge + /// @param _l1Address Address of the L1 token that can be deposited to mint this L2 WETH. + /// @param name_ The name of the token. + /// @param symbol_ The symbol of the token. + /// Note: The decimals are hardcoded to 18, the same as on Ether. + function bridgeInitialize( + address _l2Bridge, + address _l1Address, + string memory name_, + string memory symbol_ + ) external initializer { + require(_l1Address != address(0), "L1 WETH token address can not be zero"); + l2Bridge = _l2Bridge; + l1Address = _l1Address; + + // Set decoded values for name and symbol. + __ERC20_init_unchained(name_, symbol_); + + // Set the name for EIP-712 signature. + __ERC20Permit_init(name_); + + emit BridgeInitialize(_l1Address, name_, symbol_, 18); + } + + modifier onlyBridge() { + require(msg.sender == l2Bridge, "permission denied"); // Only L2 bridge can call this method + _; + } + + /// @notice Function for minting tokens on L2, is implemented to be compatible with StandardToken interface. + /// Note: Use `deposit`/`depositTo` methods instead. + function bridgeMint( + address, // _to + uint256 // _amount + ) external override onlyBridge { + revert( + "bridgeMint is not implemented! Use deposit/depositTo methods instead." + ); + } + + /// @dev Burn tokens from a given account and send the same amount of Ether to the bridge. + /// @param _from The account from which tokens will be burned. + /// @param _amount The amount that will be burned. + /// @notice Should be called by the bridge before withdrawing tokens to L1. + function bridgeBurn( + address _from, + uint256 _amount + ) external override onlyBridge { + // burns tokens from "_from" WETH contract + _burn(_from, _amount); + // sends Ether to the bridge + (bool success, ) = msg.sender.call{value: _amount}(""); + require(success, "Failed withdrawal"); + + emit BridgeBurn(_from, _amount); + } + + /// @notice Deposit Ether to mint WETH. + function deposit() external payable override { + depositTo(msg.sender); + } + + /// @notice Withdraw WETH to get Ether. + function withdraw(uint256 _amount) external override { + withdrawTo(msg.sender, _amount); + } + + /// @notice Deposit Ether to mint WETH to a given account. + function depositTo(address _to) public payable override { + _mint(_to, msg.value); + } + + /// @notice Withdraw WETH to get Ether to a given account. + /// burns sender's tokens and sends Ether to the given account + function withdrawTo(address _to, uint256 _amount) public override { + _burn(msg.sender, _amount); + (bool success, ) = _to.call{value: _amount}(""); + require(success, "Failed withdrawal"); + } + + /// @dev Fallback function to allow receiving Ether. + receive() external payable { + depositTo(msg.sender); + } +} diff --git a/zksync/contracts/bridge/L2WethBridge.sol b/zksync/contracts/bridge/L2WethBridge.sol new file mode 100644 index 000000000..a95971e68 --- /dev/null +++ b/zksync/contracts/bridge/L2WethBridge.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import "./interfaces/IL1WethBridge.sol"; +import "./interfaces/IL2WethBridge.sol"; +import "./interfaces/IL2Weth.sol"; +import "./interfaces/IL2StandardToken.sol"; +import "./interfaces/IEthToken.sol"; + +import "../vendor/AddressAliasHelper.sol"; +import { L2Weth } from "./L2Weth.sol"; +import { L2ContractHelper } from "../L2ContractHelper.sol"; + +/// @title L2 WETH Bridge +/// @author Matter Labs +contract L2WethBridge is IL2WethBridge, Initializable { + /// @dev The address of the L1 bridge counterpart. + address public override l1WethBridge; + + /// @dev WETH token address on L1. + address public override l1WethAddress; + + /// @dev WETH token address on L2. + address public override l2WethAddress; + + /// @dev ETH token address on L2. + address public constant l2EthAddress = address(0x800a); + + /// @dev Contract is expected to be used as proxy implementation. + /// @dev Disable the initialization to prevent Parity hack. + constructor() { + _disableInitializers(); + } + + function initialize( + address _l1WethBridge, + address _l1WethAddress, + address _governor + ) external initializer { + require(_l1WethBridge != address(0), "L1 WETH bridge address can not be zero"); + require(_l1WethAddress != address(0), "L1 WETH address can not be zero"); + require(_governor != address(0), "Governor address can not be zero"); + + l1WethBridge = _l1WethBridge; + l1WethAddress = _l1WethAddress; + + // Deploy L2 WETH token. + address l2Weth = address(new L2Weth{salt: bytes32(0)}()); + + // Initialization data for L2 WETH token. + // abi.encodeCall is not supported by Solidity versions below 0.8.11 + bytes memory initializationData = abi.encodeWithSelector( + L2Weth.bridgeInitialize.selector, + address(this), _l1WethAddress, "Wrapped Ether", "WETH" + ); + + // Deploy L2 WETH token proxy. + l2WethAddress = address(new TransparentUpgradeableProxy{salt: bytes32(0)}(l2Weth, _governor, initializationData)); + } + + /// @notice Initiate the withdrawal of WETH from L2 to L1 by sending a message to L1 and calling withdraw on L2EthToken contract + /// @param _l1Receiver The account address that would receive the WETH on L1 + /// @param _amount Total amount of WETH to withdraw + function withdraw(address _l1Receiver, uint256 _amount) external { + require(_l1Receiver != address(0), "L1 receiver address can not be zero"); + require(_amount > 0, "Amount can not be zero"); + + // Burn WETH on L2. + IL2StandardToken(l2WethAddress).bridgeBurn(msg.sender, _amount); + // Withdraw ETH to L1 bridge. + IEthToken(l2EthAddress).withdraw{value: _amount}(l1WethBridge); + + // Send a message to L1 to finalize the withdrawal. + bytes memory message = _getL1WithdrawalMessage(msg.sender, _l1Receiver, _amount); + L2ContractHelper.sendMessageToL1(message); + + emit WithdrawalInitiated(msg.sender, _l1Receiver, l2WethAddress, _amount); + } + + /// @notice Finalize the deposit of WETH from L1 to L2 by calling deposit on L2Weth contract + /// @param _l1Sender The account address that initiated the deposit on L1 + /// @param _l2Receiver The account address that would receive the WETH on L2 + /// @param _amount Total amount of WETH to deposit + function finalizeDeposit( + address _l1Sender, + address _l2Receiver, + uint256 _amount + ) external payable { + require(AddressAliasHelper.undoL1ToL2Alias(msg.sender) == l1WethBridge, "Only L1 WETH bridge can call this function"); + require(_l1Sender != address(0), "L1 sender address can not be zero"); + require(_l2Receiver != address(0), "L2 receiver address can not be zero"); + require(msg.value == _amount, "Amount mismatch"); + + // Deposit WETH to L2 receiver. + IL2Weth(l2WethAddress).depositTo{value: msg.value}(_l2Receiver); + + emit FinalizeDeposit(_l1Sender, _l2Receiver, l2WethAddress, _amount); + } + + /// @notice Get withdrawal message for L1 + /// @param _l2Sender The account address that would send the WETH on L2 + /// @param _l1Receiver The account address that would receive the WETH on L1 + /// @param _amount Total amount of WETH to withdraw + /// @return Message for L1 + function _getL1WithdrawalMessage( + address _l2Sender, + address _l1Receiver, + uint256 _amount + ) internal pure returns (bytes memory) { + return abi.encodePacked(IL1WethBridge.finalizeWithdrawal.selector, _l2Sender, _l1Receiver, _amount); + } + + receive() external payable {} +} \ No newline at end of file diff --git a/zksync/contracts/bridge/interfaces/IEthToken.sol b/zksync/contracts/bridge/interfaces/IEthToken.sol new file mode 100644 index 000000000..7d01f150d --- /dev/null +++ b/zksync/contracts/bridge/interfaces/IEthToken.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IEthToken { + function balanceOf(uint256) external view returns (uint256); + + function transferFromTo(address _from, address _to, uint256 _amount) external; + + function totalSupply() external view returns (uint256); + + function name() external pure returns (string memory); + + function symbol() external pure returns (string memory); + + function decimals() external pure returns (uint8); + + function mint(address _account, uint256 _amount) external; + + function withdraw(address _l1Receiver) external payable; + + event Mint(address indexed account, uint256 amount); + + event Transfer(address indexed from, address indexed to, uint256 value); + + event Withdrawal(address indexed _l2Sender, address indexed _l1Receiver, uint256 _amount); +} diff --git a/zksync/contracts/bridge/interfaces/IL1WethBridge.sol b/zksync/contracts/bridge/interfaces/IL1WethBridge.sol new file mode 100644 index 000000000..198d14027 --- /dev/null +++ b/zksync/contracts/bridge/interfaces/IL1WethBridge.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @author Matter Labs +interface IL1WethBridge { + struct FinalizeWithdrawalL2MessageIndexes { + uint256 ethL2MessageIndex; + uint256 wethL2MessageIndex; + } + + struct FinalizeWithdrawalMessages { + bytes ethMessage; + bytes wethMessage; + } + + struct FinalizeWithdrawalMerkleProofs { + bytes32[] ethProof; + bytes32[] wethProof; + } + + event DepositInitiated(address indexed from, address indexed to, address indexed l1Token, uint256 amount); + + event WithdrawalFinalized(address indexed to, address indexed l1Token, uint256 amount); + + event ClaimedFailedDeposit(address indexed to, address indexed l1Token, uint256 amount); + + function isWithdrawalFinalized(uint256 _l2BlockNumber, uint256 _l2MessageIndex) external view returns (bool); + + function deposit( + address _l2Receiver, + uint256 _amount, + uint256 _l2TxGasLimit, + uint256 _l2TxGasPerPubdataByte + ) external payable returns (bytes32 txHash); + + function claimFailedDeposit( + address _depositSender, + bytes32 _l2TxHash, + uint256 _l2BlockNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBlock, + bytes32[] calldata _merkleProof + ) external; + + function finalizeWithdrawal( + uint256 _l2BlockNumber, + FinalizeWithdrawalL2MessageIndexes calldata _l2MessageIndexes, + uint16 _l2TxNumberInBlock, + FinalizeWithdrawalMessages calldata _messages, + FinalizeWithdrawalMerkleProofs calldata _merkleProofs + ) external; +} diff --git a/zksync/contracts/bridge/interfaces/IL2StandardToken.sol b/zksync/contracts/bridge/interfaces/IL2StandardToken.sol index 5edb43c2b..eb5e4de23 100644 --- a/zksync/contracts/bridge/interfaces/IL2StandardToken.sol +++ b/zksync/contracts/bridge/interfaces/IL2StandardToken.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.0; interface IL2StandardToken { + event BridgeInitialize(address indexed l1Token, string name, string symbol, uint8 decimals); + event BridgeMint(address indexed _account, uint256 _amount); event BridgeBurn(address indexed _account, uint256 _amount); diff --git a/zksync/contracts/bridge/interfaces/IL2Weth.sol b/zksync/contracts/bridge/interfaces/IL2Weth.sol new file mode 100644 index 000000000..ce7a54c10 --- /dev/null +++ b/zksync/contracts/bridge/interfaces/IL2Weth.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IL2Weth { + function deposit() external payable; + + function withdraw(uint256 _amount) external; + + function depositTo(address _to) external payable; + + function withdrawTo(address _to, uint256 _amount) external; +} \ No newline at end of file diff --git a/zksync/contracts/bridge/interfaces/IL2WethBridge.sol b/zksync/contracts/bridge/interfaces/IL2WethBridge.sol new file mode 100644 index 000000000..2ad6b2608 --- /dev/null +++ b/zksync/contracts/bridge/interfaces/IL2WethBridge.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @author Matter Labs +interface IL2WethBridge { + event FinalizeDeposit( + address indexed l1Sender, + address indexed l2Receiver, + address indexed l2Weth, + uint256 amount + ); + + event WithdrawalInitiated( + address indexed l2Sender, + address indexed l1Receiver, + address indexed l2Weth, + uint256 amount + ); + + function finalizeDeposit( + address _l1Sender, + address _l2Receiver, + uint256 _amount + ) external payable; + + function withdraw( + address _l1Receiver, + uint256 _amount + ) external; + + function l1WethAddress() external view returns (address); + + function l2WethAddress() external view returns (address); + + function l1WethBridge() external view returns (address); +} diff --git a/zksync/package.json b/zksync/package.json index 44866f395..175452f9b 100644 --- a/zksync/package.json +++ b/zksync/package.json @@ -33,5 +33,7 @@ "deploy-testnet-paymaster": "hardhat run src/deployTestnetPaymaster.ts", "publish-bridge-preimages": "hardhat run src/publish-bridge-preimages.ts" }, - "dependencies": {} + "dependencies": { + "dotenv": "^16.0.3" + } } diff --git a/zksync/yarn.lock b/zksync/yarn.lock index 9d280f1dd..13afeb407 100644 --- a/zksync/yarn.lock +++ b/zksync/yarn.lock @@ -374,10 +374,10 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@matterlabs/hardhat-zksync-solc@^0.3.9": - version "0.3.13" - resolved "https://registry.yarnpkg.com/@matterlabs/hardhat-zksync-solc/-/hardhat-zksync-solc-0.3.13.tgz#ff3401b164ebfde6feb2a8fac8639cf39f9b4954" - integrity sha512-QuU3CfaY17SQbRxk7VvheJt+bBvWOSNaeo4hWedWXWdIa6d2Y8lQqf6QmYTFMqJg39Z+bx+uuVm2qzzLI4x0SQ== +"@matterlabs/hardhat-zksync-solc@^0.3.14-beta.3": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@matterlabs/hardhat-zksync-solc/-/hardhat-zksync-solc-0.3.14.tgz#0a32f01b4cd8631ecd8dfe0547e3ac49ab8290d5" + integrity sha512-iKuQ+vvnpv3K2lkFO41xpJcNWH0KHJ/5JbOboTlPZATVR7F3GJeHfJL+GG4wkxKXnxZczpxyQqC4rAfMKvRaDg== dependencies: "@nomiclabs/hardhat-docker" "^2.0.0" chalk "4.1.2" @@ -1902,10 +1902,10 @@ hardhat-typechain@^0.3.3: resolved "https://registry.yarnpkg.com/hardhat-typechain/-/hardhat-typechain-0.3.5.tgz#8e50616a9da348b33bd001168c8fda9c66b7b4af" integrity sha512-w9lm8sxqTJACY+V7vijiH+NkPExnmtiQEjsV9JKD1KgMdVk2q8y+RhvU/c4B7+7b1+HylRUCxpOIvFuB3rE4+w== -hardhat@^2.12.6: - version "2.12.6" - resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.12.6.tgz#ea3c058bbd81850867389d10f76037cfa52a0019" - integrity sha512-0Ent1O5DsPgvaVb5sxEgsQ3bJRt/Ex92tsoO+xjoNH2Qc4bFmhI5/CHVlFikulalxOPjNmw5XQ2vJFuVQFESAA== +hardhat@=2.12.4: + version "2.12.4" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.12.4.tgz#e539ba58bee9ba1a1ced823bfdcec0b3c5a3e70f" + integrity sha512-rc9S2U/4M+77LxW1Kg7oqMMmjl81tzn5rNFARhbXKUA1am/nhfMJEujOjuKvt+ZGMiZ11PYSe8gyIpB/aRNDgw== dependencies: "@ethersproject/abi" "^5.1.2" "@metamask/eth-sig-util" "^4.0.0" @@ -1954,7 +1954,7 @@ hardhat@^2.12.6: source-map-support "^0.5.13" stacktrace-parser "^0.1.10" tsort "0.0.1" - undici "^5.14.0" + undici "^5.4.0" uuid "^8.3.2" ws "^7.4.6" @@ -3201,10 +3201,10 @@ typical@^2.6.0, typical@^2.6.1: resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" integrity sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg== -undici@^5.14.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.15.0.tgz#cb8437c43718673a8be59df0fdd4856ff6689283" - integrity sha512-wCAZJDyjw9Myv+Ay62LAoB+hZLPW9SmKbQkbHIhMw/acKSlpn7WohdMUc/Vd4j1iSMBO0hWwU8mjB7a5p5bl8g== +undici@^5.4.0: + version "5.20.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.20.0.tgz#6327462f5ce1d3646bcdac99da7317f455bcc263" + integrity sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g== dependencies: busboy "^1.6.0"