Skip to content

Commit

Permalink
custom gas token implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
thomas-nguy committed Mar 14, 2024
1 parent d85a73a commit 106875b
Show file tree
Hide file tree
Showing 26 changed files with 325 additions and 163 deletions.
27 changes: 16 additions & 11 deletions l1-contracts/contracts/bridge/L1ERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {BridgeInitializationHelper} from "./libraries/BridgeInitializationHelper
import {IZkSync} from "../zksync/interfaces/IZkSync.sol";
import {TxStatus} from "../zksync/interfaces/IMailbox.sol";
import {L2Message} from "../zksync/Storage.sol";
import {L2Transaction} from "../zksync/libraries/L2Transaction.sol";
import {UnsafeBytes} from "../common/libraries/UnsafeBytes.sol";
import {L2ContractHelper} from "../common/libraries/L2ContractHelper.sol";
import {ReentrancyGuard} from "../common/ReentrancyGuard.sol";
Expand All @@ -42,6 +43,9 @@ contract L1ERC20Bridge is IL1Bridge, IL1BridgeLegacy, ReentrancyGuard {
mapping(address account => mapping(address l1Token => mapping(bytes32 depositL2TxHash => uint256 amount)))
internal depositAmount;

/// @dev baseToken l1 address
address public baseTokenAddress;

/// @dev The address of deployed L2 bridge counterpart
address public l2Bridge;

Expand All @@ -62,7 +66,8 @@ contract L1ERC20Bridge is IL1Bridge, IL1BridgeLegacy, ReentrancyGuard {

/// @dev Contract is expected to be used as proxy implementation.
/// @dev Initialize the implementation to prevent Parity hack.
constructor(IZkSync _zkSync) reentrancyGuardInitializer {
constructor(address _baseTokenAddress, IZkSync _zkSync) reentrancyGuardInitializer {
baseTokenAddress = _baseTokenAddress;
zkSync = _zkSync;
}

Expand All @@ -85,13 +90,11 @@ contract L1ERC20Bridge is IL1Bridge, IL1BridgeLegacy, ReentrancyGuard {
address _governor,
uint256 _deployBridgeImplementationFee,
uint256 _deployBridgeProxyFee
) external payable reentrancyGuardInitializer {
) external reentrancyGuardInitializer {
require(_l2TokenBeacon != address(0), "nf");
require(_governor != address(0), "nh");
// We are expecting to see the exact three bytecodes that are needed to initialize the bridge
require(_factoryDeps.length == 3, "mk");
// The caller miscalculated deploy transactions fees
require(msg.value == _deployBridgeImplementationFee + _deployBridgeProxyFee, "fee");
l2TokenProxyBytecodeHash = L2ContractHelper.hashL2Bytecode(_factoryDeps[2]);
l2TokenBeacon = _l2TokenBeacon;

Expand Down Expand Up @@ -148,7 +151,7 @@ contract L1ERC20Bridge is IL1Bridge, IL1BridgeLegacy, ReentrancyGuard {
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 @@ -161,6 +164,8 @@ contract L1ERC20Bridge is IL1Bridge, IL1BridgeLegacy, ReentrancyGuard {
/// @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 _l1Amount The gas token amount to be transferred from L1 to L2, it should be enough to cover the gas cost of the L2 transaction
/// it will also be the address to receive `_l2Value`. If zero, the refund will be sent to the sender of the 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.
Expand All @@ -181,8 +186,10 @@ contract L1ERC20Bridge is IL1Bridge, IL1BridgeLegacy, ReentrancyGuard {
uint256 _amount,
uint256 _l2TxGasLimit,
uint256 _l2TxGasPerPubdataByte,
address _refundRecipient
address _refundRecipient,
uint256 _l1Amount
) public payable nonReentrant returns (bytes32 l2TxHash) {
require(_l1Token != baseTokenAddress, "l1Token cannot be base token");
require(_amount != 0, "2T"); // empty deposit amount
uint256 amount = _depositFunds(msg.sender, IERC20(_l1Token), _amount);
require(amount == _amount, "1T"); // The token has non-standard transfer logic
Expand All @@ -196,13 +203,11 @@ contract L1ERC20Bridge is IL1Bridge, IL1BridgeLegacy, ReentrancyGuard {
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,
_l1Amount
);

// Save the deposited amount to claim funds on L1 if the deposit failed on L2
Expand Down
12 changes: 6 additions & 6 deletions l1-contracts/contracts/bridge/L1WethBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {IL2WethBridge} from "./interfaces/IL2WethBridge.sol";
import {IL2Bridge} from "./interfaces/IL2Bridge.sol";
import {IWETH9} from "./interfaces/IWETH9.sol";
import {IZkSync} from "../zksync/interfaces/IZkSync.sol";
import {L2Transaction} from "../zksync/libraries/L2Transaction.sol";

import {BridgeInitializationHelper} from "./libraries/BridgeInitializationHelper.sol";

Expand Down Expand Up @@ -161,7 +162,8 @@ contract L1WethBridge is IL1Bridge, ReentrancyGuard {
uint256 _amount,
uint256 _l2TxGasLimit,
uint256 _l2TxGasPerPubdataByte,
address _refundRecipient
address _refundRecipient,
uint256 _l1Amount
) external payable nonReentrant returns (bytes32 txHash) {
require(_l1Token == l1WethAddress, "Invalid L1 token address");
require(_amount != 0, "Amount cannot be zero");
Expand All @@ -182,13 +184,11 @@ contract L1WethBridge is IL1Bridge, 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,
_l1Amount
);

emit DepositInitiated(txHash, msg.sender, _l2Receiver, _l1Token, _amount);
Expand Down
3 changes: 2 additions & 1 deletion l1-contracts/contracts/bridge/interfaces/IL1Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ interface IL1Bridge {
uint256 _amount,
uint256 _l2TxGasLimit,
uint256 _l2TxGasPerPubdataByte,
address _refundRecipient
address _refundRecipient,
uint256 _l1Amount
) external payable returns (bytes32 txHash);

function claimFailedDeposit(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "../../vendor/AddressAliasHelper.sol";
import "../../common/libraries/L2ContractHelper.sol";
import {L2_DEPLOYER_SYSTEM_CONTRACT_ADDR} from "../../common/L2ContractAddresses.sol";
import "../../common/interfaces/IL2ContractDeployer.sol";
import {L2Transaction} from "../../zksync/libraries/L2Transaction.sol";

/// @author Matter Labs
/// @custom:security-contact [email protected]
Expand Down Expand Up @@ -38,14 +39,16 @@ library BridgeInitializationHelper {
IL2ContractDeployer.create2,
(bytes32(0), _bytecodeHash, _constructorData)
);
_zkSync.requestL2Transaction{value: _deployTransactionFee}(
L2_DEPLOYER_SYSTEM_CONTRACT_ADDR,
0,
_zkSync.requestL2Transaction(
L2Transaction(
L2_DEPLOYER_SYSTEM_CONTRACT_ADDR,
0,
DEPLOY_L2_BRIDGE_COUNTERPART_GAS_LIMIT,
REQUIRED_L2_GAS_PRICE_PER_PUBDATA),
deployCalldata,
DEPLOY_L2_BRIDGE_COUNTERPART_GAS_LIMIT,
REQUIRED_L2_GAS_PRICE_PER_PUBDATA,
_factoryDeps,
msg.sender
msg.sender,
_deployTransactionFee
);

deployedAddress = L2ContractHelper.computeCreate2Address(
Expand Down
22 changes: 22 additions & 0 deletions l1-contracts/contracts/dev-contracts/CronosTestnet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity 0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract CronosTestnet is ERC20, Ownable {
uint8 private _decimals = 18;

constructor() ERC20("Cronos Testnet", "TCRO"){
}

function mint(address dest, uint wad) public onlyOwner returns (bool) {
_mint(dest, wad);
return true;
}

function decimals() public view override returns (uint8) {
return _decimals;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {IMailbox} from "../../zksync/interfaces/IMailbox.sol";

/// @author Matter Labs
contract L1ERC20BridgeTest is L1ERC20Bridge {
constructor(IZkSync _zkSync) L1ERC20Bridge(_zkSync) {}
constructor(address _baseTokenAddress, IZkSync _zkSync) L1ERC20Bridge(_baseTokenAddress, _zkSync) {}

function getZkSyncMailbox() public view returns (IMailbox) {
return zkSync;
Expand Down
2 changes: 2 additions & 0 deletions l1-contracts/contracts/zksync/DiamondInit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ contract DiamondInit is Base {
bytes32 l2DefaultAccountBytecodeHash;
uint256 priorityTxMaxGasLimit;
uint256 initialProtocolVersion;
address baseTokenAddress;
FeeParams feeParams;
address blobVersionedHashRetriever;
}
Expand All @@ -64,6 +65,7 @@ contract DiamondInit is Base {
s.verifier = _initalizeData.verifier;
s.governor = _initalizeData.governor;
s.admin = _initalizeData.admin;
s.baseTokenAddress = _initalizeData.baseTokenAddress;

// We need to initialize the state hash because it is used in the commitment of the next batch
IExecutor.StoredBatchInfo memory storedBatchZero = IExecutor.StoredBatchInfo(
Expand Down
2 changes: 2 additions & 0 deletions l1-contracts/contracts/zksync/Storage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ struct AppStorage {
address admin;
/// @notice Address that the governor or admin proposed as one that will replace admin role
address pendingAdmin;
/// @dev baseToken l1 address
address baseTokenAddress;
/// @dev Fee params used to derive gasPrice for the L1->L2 transactions. For L2 transactions,
/// the bootloader gives enough freedom to the operator.
FeeParams feeParams;
Expand Down
78 changes: 50 additions & 28 deletions l1-contracts/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.20;

import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import {IMailbox, TxStatus} from "../interfaces/IMailbox.sol";
import {Merkle} from "../libraries/Merkle.sol";
Expand All @@ -16,6 +17,7 @@ import {AddressAliasHelper} from "../../vendor/AddressAliasHelper.sol";
import {Base} from "./Base.sol";
import {REQUIRED_L2_GAS_PRICE_PER_PUBDATA, L1_GAS_PER_PUBDATA_BYTE, L2_L1_LOGS_TREE_DEFAULT_LEAF_HASH, PRIORITY_OPERATION_L2_TX_TYPE, PRIORITY_EXPIRATION, MAX_NEW_FACTORY_DEPS} from "../Config.sol";
import {L2_BOOTLOADER_ADDRESS, L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR, L2_ETH_TOKEN_SYSTEM_CONTRACT_ADDR} from "../../common/L2ContractAddresses.sol";
import {L2Transaction} from "../libraries/L2Transaction.sol";

// While formally the following import is not used, it is needed to inherit documentation from it
import {IBase} from "../interfaces/IBase.sol";
Expand All @@ -26,6 +28,7 @@ import {IBase} from "../interfaces/IBase.sol";
contract MailboxFacet is Base, IMailbox {
using UncheckedMath for uint256;
using PriorityQueue for PriorityQueue.Queue;
using SafeERC20 for IERC20;

/// @inheritdoc IBase
string public constant override getName = "MailboxFacet";
Expand Down Expand Up @@ -84,11 +87,15 @@ 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 batch number
Expand Down Expand Up @@ -140,6 +147,11 @@ 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 _gasPerPubdata The price for each pubdata byte in L2 gas
Expand Down Expand Up @@ -190,13 +202,11 @@ contract MailboxFacet is Base, IMailbox {

/// @inheritdoc IMailbox
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 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 @@ -210,31 +220,43 @@ contract MailboxFacet is Base, IMailbox {
// 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");

_lockDeposit(_baseAmount);


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

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

function _lockDeposit(uint256 _baseAmount) internal {
if (s.baseTokenAddress != address(0)) {
IERC20(s.baseTokenAddress).safeTransferFrom(tx.origin, address(this), _baseAmount);
}
}

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 @@ -246,9 +268,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 `msg.value` doesn't cover the transaction cost
}

// If the `_refundRecipient` is not provided, we use the `_sender` as the recipient.
Expand All @@ -260,12 +282,12 @@ 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.valueToMint = _valueToMint;
params.refundRecipient = refundRecipient;

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

0 comments on commit 106875b

Please sign in to comment.