Skip to content

Commit

Permalink
custom base token solution draft
Browse files Browse the repository at this point in the history
  • Loading branch information
thomas-nguy committed Sep 5, 2023
1 parent fa37065 commit 83da0a4
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 64 deletions.
14 changes: 7 additions & 7 deletions ethereum/contracts/bridge/L1ERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`.
Expand All @@ -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);
Expand All @@ -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
Expand Down
13 changes: 6 additions & 7 deletions ethereum/contracts/bridge/L1WethBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion ethereum/contracts/bridge/interfaces/IL1Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
51 changes: 51 additions & 0 deletions ethereum/contracts/bridge/interfaces/IL1WethBridge.sol
Original file line number Diff line number Diff line change
@@ -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);
}
27 changes: 18 additions & 9 deletions ethereum/contracts/bridge/libraries/BridgeInitializationHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion ethereum/contracts/zksync/DiamondInit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
2 changes: 2 additions & 0 deletions ethereum/contracts/zksync/Storage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
91 changes: 57 additions & 34 deletions ethereum/contracts/zksync/facets/Mailbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand All @@ -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.
Expand All @@ -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");
Expand All @@ -266,28 +285,32 @@ 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
uint256 txId = s.priorityQueue.getTotalPriorityTxs();

// 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.
Expand All @@ -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);
}
Expand Down
Loading

0 comments on commit 83da0a4

Please sign in to comment.