diff --git a/ethereum/contracts/bridge/L1ERC20Bridge.sol b/ethereum/contracts/bridge/L1ERC20Bridge.sol index 5eb492bd1b..25cf914307 100644 --- a/ethereum/contracts/bridge/L1ERC20Bridge.sol +++ b/ethereum/contracts/bridge/L1ERC20Bridge.sol @@ -144,7 +144,7 @@ contract L1ERC20Bridge is IL1Bridge, IL1BridgeLegacy, AllowListed, ReentrancyGua uint256 _l2TxGasLimit, uint256 _l2TxGasPerPubdataByte ) external payable returns (bytes32 l2TxHash) { - l2TxHash = deposit(_l2Receiver, _l1Token, _amount, _l2TxGasLimit, _l2TxGasPerPubdataByte, address(0)); + l2TxHash = deposit(_l2Receiver, _l1Token, _amount, _l2TxGasLimit, _l2TxGasPerPubdataByte, address(0),0); } /// @notice Initiates a deposit by locking funds on the contract and sending the request @@ -155,6 +155,7 @@ contract L1ERC20Bridge is IL1Bridge, IL1BridgeLegacy, AllowListed, ReentrancyGua /// @param _l2TxGasLimit The L2 gas limit to be used in the corresponding L2 transaction /// @param _l2TxGasPerPubdataByte The gasPerPubdataByteLimit to be used in the corresponding L2 transaction /// @param _refundRecipient The address on L2 that will receive the refund for the transaction. + /// @param _baseAmount The amount of base token to be transferred from L1 to L2, it should be enough to cover the gas cost of the L2 transaction. /// @dev If the L2 deposit finalization transaction fails, the `_refundRecipient` will receive the `_l2Value`. /// Please note, the contract may change the refund recipient's address to eliminate sending funds to addresses out of control. /// - If `_refundRecipient` is a contract on L1, the refund will be sent to the aliased `_refundRecipient`. @@ -171,7 +172,8 @@ contract L1ERC20Bridge is IL1Bridge, IL1BridgeLegacy, AllowListed, ReentrancyGua uint256 _amount, uint256 _l2TxGasLimit, uint256 _l2TxGasPerPubdataByte, - address _refundRecipient + address _refundRecipient, + uint256 _baseAmount ) public payable nonReentrant senderCanCallFunction(allowList) returns (bytes32 l2TxHash) { require(_amount != 0, "2T"); // empty deposit amount uint256 amount = _depositFunds(msg.sender, IERC20(_l1Token), _amount); @@ -188,13 +190,11 @@ contract L1ERC20Bridge is IL1Bridge, IL1BridgeLegacy, AllowListed, ReentrancyGua refundRecipient = msg.sender != tx.origin ? AddressAliasHelper.applyL1ToL2Alias(msg.sender) : msg.sender; } l2TxHash = zkSync.requestL2Transaction{value: msg.value}( - l2Bridge, - 0, // L2 msg.value + L2Transaction(l2Bridge, 0, _l2TxGasLimit, _l2TxGasPerPubdataByte), l2TxCalldata, - _l2TxGasLimit, - _l2TxGasPerPubdataByte, new bytes[](0), - refundRecipient + refundRecipient, + _baseAmount ); // Save the deposited amount to claim funds on L1 if the deposit failed on L2 diff --git a/ethereum/contracts/bridge/L1WethBridge.sol b/ethereum/contracts/bridge/L1WethBridge.sol index c6335e7832..a8cb8b9b73 100644 --- a/ethereum/contracts/bridge/L1WethBridge.sol +++ b/ethereum/contracts/bridge/L1WethBridge.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.13; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "./interfaces/IL1Bridge.sol"; +import "./interfaces/IL1WethBridge.sol"; import "./interfaces/IL2WethBridge.sol"; import "./interfaces/IL2Bridge.sol"; import "./interfaces/IWETH9.sol"; @@ -33,7 +33,7 @@ import "../vendor/AddressAliasHelper.sol"; /// @dev For withdrawals, the contract receives ETH from the L2 WETH bridge contract, wraps it into /// WETH, and sends the WETH to the L1 recipient. /// @dev The `L1WethBridge` contract works in conjunction with its L2 counterpart, `L2WethBridge`. -contract L1WethBridge is IL1Bridge, AllowListed, ReentrancyGuard { +contract L1WethBridge is IL1WethBridge, AllowListed, ReentrancyGuard { using SafeERC20 for IERC20; /// @dev Event emitted when ETH is received by the contract. @@ -161,6 +161,7 @@ contract L1WethBridge is IL1Bridge, AllowListed, ReentrancyGuard { ) external payable nonReentrant senderCanCallFunction(allowList) returns (bytes32 txHash) { require(_l1Token == l1WethAddress, "Invalid L1 token address"); require(_amount != 0, "Amount cannot be zero"); + //require(zkSync.baseTokenAddress() == address(0), "Base token has to be ETH"); // Deposit WETH tokens from the depositor address to the smart contract address IERC20(l1WethAddress).safeTransferFrom(msg.sender, address(this), _amount); @@ -178,13 +179,11 @@ contract L1WethBridge is IL1Bridge, AllowListed, ReentrancyGuard { refundRecipient = msg.sender != tx.origin ? AddressAliasHelper.applyL1ToL2Alias(msg.sender) : msg.sender; } txHash = zkSync.requestL2Transaction{value: _amount + msg.value}( - l2Bridge, - _amount, + L2Transaction(l2Bridge, 0, _l2TxGasLimit, _l2TxGasPerPubdataByte), l2TxCalldata, - _l2TxGasLimit, - _l2TxGasPerPubdataByte, new bytes[](0), - refundRecipient + refundRecipient, + 0 ); emit DepositInitiated(txHash, msg.sender, _l2Receiver, _l1Token, _amount); diff --git a/ethereum/contracts/bridge/interfaces/IL1Bridge.sol b/ethereum/contracts/bridge/interfaces/IL1Bridge.sol index 6349dd635a..c8d5e8c9db 100644 --- a/ethereum/contracts/bridge/interfaces/IL1Bridge.sol +++ b/ethereum/contracts/bridge/interfaces/IL1Bridge.sol @@ -24,7 +24,8 @@ interface IL1Bridge { uint256 _amount, uint256 _l2TxGasLimit, uint256 _l2TxGasPerPubdataByte, - address _refundRecipient + address _refundRecipient, + uint256 _baseAmount ) external payable returns (bytes32 txHash); function claimFailedDeposit( diff --git a/ethereum/contracts/bridge/interfaces/IL1WethBridge.sol b/ethereum/contracts/bridge/interfaces/IL1WethBridge.sol new file mode 100644 index 0000000000..96872bead9 --- /dev/null +++ b/ethereum/contracts/bridge/interfaces/IL1WethBridge.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.13; + +/// @author Matter Labs +interface IL1WethBridge { + event DepositInitiated( + bytes32 indexed l2DepositTxHash, + address indexed from, + address indexed to, + address 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, + address _l1Token, + uint256 _amount, + uint256 _l2TxGasLimit, + uint256 _l2TxGasPerPubdataByte, + address _refundRecipient + ) external payable returns (bytes32 txHash); + + function claimFailedDeposit( + address _depositSender, + address _l1Token, + bytes32 _l2TxHash, + uint256 _l2BlockNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBlock, + bytes32[] calldata _merkleProof + ) external; + + function finalizeWithdrawal( + uint256 _l2BlockNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBlock, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external; + + function l2TokenAddress(address _l1Token) external view returns (address); + + function l2Bridge() external view returns (address); +} diff --git a/ethereum/contracts/bridge/libraries/BridgeInitializationHelper.sol b/ethereum/contracts/bridge/libraries/BridgeInitializationHelper.sol index 5643458c11..c6b6a96f08 100644 --- a/ethereum/contracts/bridge/libraries/BridgeInitializationHelper.sol +++ b/ethereum/contracts/bridge/libraries/BridgeInitializationHelper.sol @@ -37,15 +37,24 @@ library BridgeInitializationHelper { IL2ContractDeployer.create2, (bytes32(0), _bytecodeHash, _constructorData) ); - _zkSync.requestL2Transaction{value: _deployTransactionFee}( - L2_DEPLOYER_SYSTEM_CONTRACT_ADDR, - 0, - deployCalldata, - DEPLOY_L2_BRIDGE_COUNTERPART_GAS_LIMIT, - REQUIRED_L2_GAS_PRICE_PER_PUBDATA, - _factoryDeps, - msg.sender - ); + + if (_zkSync.baseTokenAddress() == address(0)) { + _zkSync.requestL2Transaction{value: _deployTransactionFee}( + L2Transaction(L2_DEPLOYER_SYSTEM_CONTRACT_ADDR, 0, DEPLOY_L2_BRIDGE_COUNTERPART_GAS_LIMIT,REQUIRED_L2_GAS_PRICE_PER_PUBDATA ), + deployCalldata, + _factoryDeps, + msg.sender, + 0 + ); + } else { + _zkSync.requestL2Transaction{value: 0}( + L2Transaction(L2_DEPLOYER_SYSTEM_CONTRACT_ADDR, 0, DEPLOY_L2_BRIDGE_COUNTERPART_GAS_LIMIT,REQUIRED_L2_GAS_PRICE_PER_PUBDATA ), + deployCalldata, + _factoryDeps, + msg.sender, + _deployTransactionFee + ); + } deployedAddress = L2ContractHelper.computeCreate2Address( // Apply the alias to the address of the bridge contract, to get the `msg.sender` in L2. diff --git a/ethereum/contracts/zksync/DiamondInit.sol b/ethereum/contracts/zksync/DiamondInit.sol index d9fb9021de..47b7cf138e 100644 --- a/ethereum/contracts/zksync/DiamondInit.sol +++ b/ethereum/contracts/zksync/DiamondInit.sol @@ -39,7 +39,8 @@ contract DiamondInit is Base { bool _zkPorterIsAvailable, bytes32 _l2BootloaderBytecodeHash, bytes32 _l2DefaultAccountBytecodeHash, - uint256 _priorityTxMaxGasLimit + uint256 _priorityTxMaxGasLimit, + address _baseTokenAddress ) external reentrancyGuardInitializer returns (bytes32) { require(address(_verifier) != address(0), "vt"); require(_governor != address(0), "vy"); diff --git a/ethereum/contracts/zksync/Storage.sol b/ethereum/contracts/zksync/Storage.sol index 27e13906b9..1fd9fd0568 100644 --- a/ethereum/contracts/zksync/Storage.sol +++ b/ethereum/contracts/zksync/Storage.sol @@ -133,4 +133,6 @@ struct AppStorage { bytes32 l2SystemContractsUpgradeTxHash; /// @dev Block number where the upgrade transaction has happened. If 0, then no upgrade transaction has happened yet. uint256 l2SystemContractsUpgradeBlockNumber; + /// @dev base token address on l1, if 0 then eth is used as base token. + address baseTokenAddress; } diff --git a/ethereum/contracts/zksync/facets/Mailbox.sol b/ethereum/contracts/zksync/facets/Mailbox.sol index d2a59c9821..f15d6dd5c6 100644 --- a/ethereum/contracts/zksync/facets/Mailbox.sol +++ b/ethereum/contracts/zksync/facets/Mailbox.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../interfaces/IMailbox.sol"; import "../libraries/Merkle.sol"; @@ -22,6 +23,7 @@ import "./Base.sol"; contract MailboxFacet is Base, IMailbox { using UncheckedMath for uint256; using PriorityQueue for PriorityQueue.Queue; + using SafeERC20 for IERC20; string public constant override getName = "MailboxFacet"; @@ -97,11 +99,16 @@ contract MailboxFacet is Base, IMailbox { /// @dev Reverts only if the transfer call failed function _withdrawFunds(address _to, uint256 _amount) internal { bool callSuccess; - // Low-level assembly call, to avoid any memory copying (save gas) - assembly { - callSuccess := call(gas(), _to, _amount, 0, 0, 0, 0) + + if (s.baseTokenAddress == address(0)) { + // Low-level assembly call, to avoid any memory copying (save gas) + assembly { + callSuccess := call(gas(), _to, _amount, 0, 0, 0, 0) + } + require(callSuccess, "pz"); + } else { + IERC20(s.baseTokenAddress).safeTransfer(_to, _amount); } - require(callSuccess, "pz"); } /// @dev Prove that a specific L2 log was sent in a specific L2 block number @@ -156,6 +163,12 @@ contract MailboxFacet is Base, IMailbox { return l2GasPrice * _l2GasLimit; } + /// @notice Return the address of the base token contract on L1. If 0 then it uses ETH. + function baseTokenAddress( + ) public view returns (address) { + return s.baseTokenAddress; + } + /// @notice Derives the price for L2 gas in ETH to be paid. /// @param _l1GasPrice The gas price on L1. /// @param _gasPricePerPubdata The price for each pubdata byte in L2 gas @@ -200,13 +213,11 @@ contract MailboxFacet is Base, IMailbox { } /// @notice Request execution of L2 transaction from L1. - /// @param _contractL2 The L2 receiver address - /// @param _l2Value `msg.value` of L2 transaction + /// @param _l2tx The L2 transaction parameters /// @param _calldata The input of the L2 transaction - /// @param _l2GasLimit Maximum amount of L2 gas that transaction can consume during execution on L2 - /// @param _l2GasPerPubdataByteLimit The maximum amount L2 gas that the operator may charge the user for single byte of pubdata. /// @param _factoryDeps An array of L2 bytecodes that will be marked as known on L2 /// @param _refundRecipient The address on L2 that will receive the refund for the transaction. + /// @param _baseAmount The base token amount to bridge along with with this transaction /// @dev If the L2 deposit finalization transaction fails, the `_refundRecipient` will receive the `_l2Value`. /// Please note, the contract may change the refund recipient's address to eliminate sending funds to addresses out of control. /// - If `_refundRecipient` is a contract on L1, the refund will be sent to the aliased `_refundRecipient`. @@ -218,13 +229,11 @@ contract MailboxFacet is Base, IMailbox { /// through the Mailbox to use or withdraw the funds from L2, and the funds would be lost. /// @return canonicalTxHash The hash of the requested L2 transaction. This hash can be used to follow the transaction status function requestL2Transaction( - address _contractL2, - uint256 _l2Value, + L2Transaction memory _l2tx, bytes calldata _calldata, - uint256 _l2GasLimit, - uint256 _l2GasPerPubdataByteLimit, bytes[] calldata _factoryDeps, - address _refundRecipient + address _refundRecipient, + uint256 _baseAmount ) external payable nonReentrant senderCanCallFunction(s.allowList) returns (bytes32 canonicalTxHash) { // Change the sender address if it is a smart contract to prevent address collision between L1 and L2. // Please note, currently zkSync address derivation is different from Ethereum one, but it may be changed in the future. @@ -233,31 +242,41 @@ contract MailboxFacet is Base, IMailbox { sender = AddressAliasHelper.applyL1ToL2Alias(msg.sender); } + if (s.baseTokenAddress != address(0)) { + // prevent stack too deep error + { + IERC20(s.baseTokenAddress).safeTransferFrom(tx.origin, address(this), _baseAmount); + } + } + // Enforcing that `_l2GasPerPubdataByteLimit` equals to a certain constant number. This is needed // to ensure that users do not get used to using "exotic" numbers for _l2GasPerPubdataByteLimit, e.g. 1-2, etc. // VERY IMPORTANT: nobody should rely on this constant to be fixed and every contract should give their users the ability to provide the // ability to provide `_l2GasPerPubdataByteLimit` for each independent transaction. // CHANGING THIS CONSTANT SHOULD BE A CLIENT-SIDE CHANGE. - require(_l2GasPerPubdataByteLimit == REQUIRED_L2_GAS_PRICE_PER_PUBDATA, "qp"); + require(_l2tx.l2GasPerPubdataByteLimit == REQUIRED_L2_GAS_PRICE_PER_PUBDATA, "qp"); // The L1 -> L2 transaction may be failed and funds will be sent to the `_refundRecipient`, // so we use `msg.value` instead of `_l2Value` as the bridged amount. - _verifyDepositLimit(msg.sender, msg.value); + if (s.baseTokenAddress == address(0)) { + _verifyDepositLimit(msg.sender, msg.value); + } else { + _verifyDepositLimit(msg.sender, _baseAmount); + } + canonicalTxHash = _requestL2Transaction( sender, - _contractL2, - _l2Value, + _l2tx, _calldata, - _l2GasLimit, - _l2GasPerPubdataByteLimit, _factoryDeps, false, - _refundRecipient + _refundRecipient, + _baseAmount ); } function _verifyDepositLimit(address _depositor, uint256 _amount) internal { - IAllowList.Deposit memory limitData = IAllowList(s.allowList).getTokenDepositLimitData(address(0)); // address(0) denotes the ETH + IAllowList.Deposit memory limitData = IAllowList(s.allowList).getTokenDepositLimitData(s.baseTokenAddress); // address(0) denotes the ETH if (!limitData.depositLimitation) return; // no deposit limitation is placed for ETH require(s.totalDepositedAmountPerUser[_depositor] + _amount <= limitData.depositCap, "d2"); @@ -266,14 +285,12 @@ contract MailboxFacet is Base, IMailbox { function _requestL2Transaction( address _sender, - address _contractAddressL2, - uint256 _l2Value, + L2Transaction memory _l2tx, bytes calldata _calldata, - uint256 _l2GasLimit, - uint256 _l2GasPerPubdataByteLimit, bytes[] calldata _factoryDeps, bool _isFree, - address _refundRecipient + address _refundRecipient, + uint256 _baseAmount ) internal returns (bytes32 canonicalTxHash) { require(_factoryDeps.length <= MAX_NEW_FACTORY_DEPS, "uj"); uint64 expirationTimestamp = uint64(block.timestamp + PRIORITY_EXPIRATION); // Safe to cast @@ -281,13 +298,19 @@ contract MailboxFacet is Base, IMailbox { // Here we manually assign fields for the struct to prevent "stack too deep" error WritePriorityOpParams memory params; + uint256 baseAmount; + if (s.baseTokenAddress == address(0)) { + baseAmount = msg.value; + } else { + baseAmount = _baseAmount; + } // Checking that the user provided enough ether to pay for the transaction. // Using a new scope to prevent "stack too deep" error { - params.l2GasPrice = _isFree ? 0 : _deriveL2GasPrice(tx.gasprice, _l2GasPerPubdataByteLimit); - uint256 baseCost = params.l2GasPrice * _l2GasLimit; - require(msg.value >= baseCost + _l2Value, "mv"); // The `msg.value` doesn't cover the transaction cost + params.l2GasPrice = _isFree ? 0 : _deriveL2GasPrice(tx.gasprice, _l2tx.l2GasPerPubdataByteLimit); + uint256 baseCost = params.l2GasPrice * _l2tx.l2GasLimit; + require(baseAmount >= baseCost + _l2tx.l2Value, "mv"); // The base amount doesn't cover the transaction cost } // If the `_refundRecipient` is not provided, we use the `_sender` as the recipient. @@ -299,13 +322,13 @@ contract MailboxFacet is Base, IMailbox { params.sender = _sender; params.txId = txId; - params.l2Value = _l2Value; - params.contractAddressL2 = _contractAddressL2; + params.l2Value = _l2tx.l2Value; + params.contractAddressL2 = _l2tx.l2Contract; params.expirationTimestamp = expirationTimestamp; - params.l2GasLimit = _l2GasLimit; - params.l2GasPricePerPubdata = _l2GasPerPubdataByteLimit; - params.valueToMint = msg.value; + params.l2GasLimit = _l2tx.l2GasLimit; + params.l2GasPricePerPubdata = _l2tx.l2GasPerPubdataByteLimit; params.refundRecipient = refundRecipient; + params.valueToMint = baseAmount; canonicalTxHash = _writePriorityOp(params, _calldata, _factoryDeps); } diff --git a/ethereum/contracts/zksync/interfaces/IMailbox.sol b/ethereum/contracts/zksync/interfaces/IMailbox.sol index 6d37d080af..39fb21d0dc 100644 --- a/ethereum/contracts/zksync/interfaces/IMailbox.sol +++ b/ethereum/contracts/zksync/interfaces/IMailbox.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; import {L2Log, L2Message} from "../Storage.sol"; +import {L2Transaction} from "../libraries/L2Transaction.sol"; import "./IBase.sol"; /// @dev The enum that represents the transaction execution status @@ -118,13 +119,11 @@ interface IMailbox is IBase { ) external; function requestL2Transaction( - address _contractL2, - uint256 _l2Value, + L2Transaction memory _l2tx, bytes calldata _calldata, - uint256 _l2GasLimit, - uint256 _l2GasPerPubdataByteLimit, bytes[] calldata _factoryDeps, - address _refundRecipient + address _refundRecipient, + uint256 _baseAmount ) external payable returns (bytes32 canonicalTxHash); function l2TransactionBaseCost( @@ -133,6 +132,9 @@ interface IMailbox is IBase { uint256 _l2GasPerPubdataByteLimit ) external view returns (uint256); + function baseTokenAddress( + ) external view returns (address); + /// @notice New priority request event. Emitted when a request is placed into the priority queue /// @param txId Serial number of the priority operation /// @param txHash keccak256 hash of encoded transaction representation diff --git a/ethereum/contracts/zksync/libraries/L2Transaction.sol b/ethereum/contracts/zksync/libraries/L2Transaction.sol new file mode 100644 index 0000000000..3cd0141845 --- /dev/null +++ b/ethereum/contracts/zksync/libraries/L2Transaction.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.13; + +/// @notice Structure that contains an L2 transaction without calldata +/// @dev used to prevent deep stack error +struct L2Transaction { + address l2Contract; //L2 transaction msg.to + uint256 l2Value; //L2 transaction msg.value + uint256 l2GasLimit; //Maximum amount of L2 gas that transaction can consume during execution on L2 + uint256 l2GasPerPubdataByteLimit; //The maximum amount L2 gas that the operator may charge the user for single byte of pubdata +} \ No newline at end of file