Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom base token #12

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this contract won't be needed if we have a custom base token


// 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);
}
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
4 changes: 3 additions & 1 deletion ethereum/contracts/zksync/DiamondInit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ 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");

s.verifier = _verifier;
s.governor = _governor;
s.baseTokenAddress = _baseTokenAddress;

// We need to initialize the state hash because it is used in the commitment of the next block
IExecutor.StoredBlockInfo memory storedBlockZero = IExecutor.StoredBlockInfo(
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;
}
90 changes: 55 additions & 35 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,44 @@ 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");

uint256 valueToMint;
if (s.baseTokenAddress == address(0)) {
valueToMint = msg.value;
} else {
valueToMint = _baseAmount;
}

// 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);
// so we use `valueToMint` instead of `_l2Value` as the bridged amount.
_verifyDepositLimit(msg.sender, valueToMint);

canonicalTxHash = _requestL2Transaction(
sender,
_contractL2,
_l2Value,
_l2tx,
_calldata,
_l2GasLimit,
_l2GasPerPubdataByteLimit,
_factoryDeps,
false,
_refundRecipient
_refundRecipient,
valueToMint
);
}

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,14 +288,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 _valueToMint
) internal returns (bytes32 canonicalTxHash) {
require(_factoryDeps.length <= MAX_NEW_FACTORY_DEPS, "uj");
uint64 expirationTimestamp = uint64(block.timestamp + PRIORITY_EXPIRATION); // Safe to cast
Expand All @@ -285,9 +305,9 @@ contract MailboxFacet is Base, IMailbox {
// 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(_valueToMint >= 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 +319,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 = _valueToMint;

canonicalTxHash = _writePriorityOp(params, _calldata, _factoryDeps);
}
Expand Down
Loading