L1 -> L2 communication


L1 -> L2 communication

This section describes the interface for interaction with zkSync from L1. It assumes that you are already familiar with the basic concepts of working with the priority queue. If you are new to this topic, you can read the conceptual introduction here. If you want to dive straight into the code, then you can read the cross-chain governance tutorial.

Note

Please note that with system update released in Feb 2023, the ergs concept is only used by the VM and the SDK (version 0.13.0 and above) whilest the API layer operates with gas.

Find more information in the changelog.

Structure

For the most common usecase, there is "baseFee" for a transaction, which basically means the minimum amount the user has to pay to the operator for him to include this transaction. It is derived based on the l2gasLimit for a transaction and the gas price on L1. In addition, whatever fee the user pays above is called layer2 tip and will be used to sort the transactions by the provided L2 fee.

At the moment, all the L1-> L2 transactions are served at the first-in-first-out basis, but in the future we will introduce "priority heap", which will allow for sorting the transactions. Basic costs are defined in gas and not in ETH, so the actual amount of ether that the submission of the transaction will cost depends on the transaction gas price. Generally the flow for calling any of these methods should be the following:

  1. Fetch the gas price that you will use to send the transaction.
  2. Get the base cost for the transaction.
  3. Send the transaction including the needed value.

Using contract interface in your project

To interact with the zkSync mailbox contract using Solidity, you need to use the zkSync contract interface. There are two main ways to get it:

  • By importing it from the @matterlabs/zksync-contracts npm package (preferred).
  • By downloading the contracts from the repoopen in new window.

The @matterlabs/zksync-contracts package can be installed by running the following command:

yarn add -D @matterlabs/zksync-contracts

In the examples below we assume that the interface is accessed via the @matterlabs/zksync-contracts npm package.

Getting the base cost

The following view function returns the amount of ETH that is needed to be supplied by the user to cover the base cost of the transaction.

function l2TransactionBaseCost(
    uint256 _gasPrice,
    uint256 _gasLimit,
    uint256 _l2GasPerPubdataByteLimit
) external view returns (uint256);
  • _gasPrice is a parameter that contains the transaction gas price.
  • _gasLimit is a parameter that contains the gas limit of the transaction call. You can learn more about gas and the zkSync fee system here.
  • _l2GasPerPubdataByteLimit is a parameter that contains the L2 gas price for each published L1 calldata byte.

Estimating the gas fee

The RPC function zks_estimateGasL1ToL2 returns an estimate of how much gas is required to complete the L1->L2 transaction. The parameters are the same as for eth_estimateGas.

eth_estimateGas is applicable for both zkSync L2 and Ethereum while zks_estimateGasL1ToL2 is only available on zkSync.

Note

If the sender is a smart contract, then during the L1->L2 the msg.sender address is aliased. Note that this is not done automatically during fee estimation, so it is your responsibility to use the zkSync aliasing utility to get the correct from address for the fee estimation.

The function is wrapped in the Provider class.

async estimateGasL1(transaction: utils.Deferrable<TransactionRequest>): Promise<BigNumber> {
    await this.getNetwork();
      const params = await utils.resolveProperties({
          transaction: this._getTransactionRequest(transaction)
      });
      if (transaction.customData != null) {
          // @ts-ignore
          params.transaction.customData = transaction.customData;
      }
      const result = await this.send('zks_estimateGasL1ToL2', [
          Provider.hexlifyTransaction(params.transaction, { from: true })
      ]);
      try {
          return BigNumber.from(result);
      } catch (error) {
          throw new Error(`bad result from backend (zks_estimateGasL1ToL2): ${result}`);
      }
}

You can use the Provider.estimateGasL1 to estimate the L2 gas needed for the L1->L2 transaction.

The function takes the following parameters:

  • from: sender address.
  • to: recipient address.
  • gas: (optional) gas used for the transaction.
  • gasPrice: (optional) gasPrice.
  • value: (optional) value sent by the transaction.
  • data: (optional) hash of the method signature and encoded parameters.

Interface

The following function returns the canonical hash or the requested transaction, that can be used to track the execution of the transaction in L2.

Use the Provider class, as described in the section above, to call the method.

function requestL2Transaction(
    address _contractL2,
    uint256 _l2Value,
    bytes calldata _calldata,
    uint256 _l2GasLimit,
    uint256 _l2GasPerPubdataByteLimit,
    bytes[] calldata _factoryDeps,
    address _refundRecipient
) external payable returns (bytes32 txHash);
  • _contractL2 is a parameter that defines the address of the contract to be called.
  • _l2Value is a parameter that defines the amount of ETH you want to pass with the call to L2. This number will be used as msg.value for the transaction.
  • _calldata is a parameter that contains the calldata of the transaction call. It can be encoded the same way as on Ethereum.
  • _l2GasLimit is a parameter that contains the gas limit of the transaction call. You can learn more about gas and the zkSync fee system here.
  • _l2GasPerPubdataByteLimit is a parameter that contains the L2 gas price per each published to L1 calldata byte.
  • _factoryDeps is a list of bytecodes. It should contain the bytecode of the contract being deployed. If the contract being deployed is a factory contract, i.e. it can deploy other contracts, the array should also contain the bytecodes of the contracts that can be deployed by it.
  • _refundRecipient is an address that receives the rest of the fee after the transaction execution. If refundRecipient == 0, L2 msg.sender is used.

Note

If the _refundRecipient is a smart contract, then during the L1->L2 the msg.sender address is aliased.

With the method call, some amount of ETH should be supplied to cover the base cost of the transaction (including the _l2Value) + layer 2 operator tip.

Tips

A successful L1 -> L2 message produces an L2Log with key = l2TxHash, and value = bytes32(1) whereas a failed L1 -> L2 message produces an L2Log with key = l2TxHash, and value = bytes32(0).

Aliasing

The L1->L2 alias function applies an alias to a given address. The L2 alias is the same alias that is applied on the L1 smart contracts during L1->L2 communication.

export function applyL1ToL2Alias(address: string): string {
    return ethers.utils.hexlify(ethers.BigNumber.from(address).add(L1_TO_L2_ALIAS_OFFSET).mod(ADDRESS_MODULO));
}

Examples

Solidity

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

// Importing zkSync contract interface
import "@matterlabs/zksync-contracts/l1/contracts/zksync/interfaces/IZkSync.sol";

contract Example {
    function callZkSync(
        // The address of the zkSync smart contract.
        // It is not recommended to hardcode it during the alpha testnet as regenesis may happen.
        address _zkSyncAddress
    ) external payable returns(bytes32 txHash) {
        IZkSync zksync = IZkSync(_zkSyncAddress);
        address someL2Contract = 0xDbA0833e8c4b37cecC177a665E9207962e337299;
        // calling L2 smart contract from L1 Example contract
        txHash = zksync.requestL2Transaction{value: msg.value}(
            // The address of the L2 contract to call
            someL2Contract,
            // We pass no ETH with the call
            0,
            // Encoding the calldata for the execute
            abi.encodeWithSignature("someMethod()"),
            // Gas limit
            10000,
            // gas price per pubdata byte
            800,
            // factory dependencies
            new bytes[](0),
            // refund address
            address(0)
        );
    }
}

zksync-web3

import { Wallet, Provider } from "zksync-web3";
import { ethers, BigNumber } from "ethers";

const TEST_PRIVATE_KEY = "";

const zkSyncProvider = new Provider("https://zksync2-testnet.zksync.dev");
const ethereumProvider = ethers.getDefaultProvider("goerli");
const wallet = new Wallet(TEST_PRIVATE_KEY, zkSyncProvider, ethereumProvider);

const gasPrice = await wallet.providerL1!.getGasPrice();

// The calldata can be encoded the same way as for Ethereum
const calldata = "0x...";
const gasLimit = BigNumber.from(1000);
const gasPerPubdataByte = BigNumber.from(800);

const txCostPrice = await wallet.getBaseCost({
  gasPrice,
  gasLimit,
  gasPerPubdataByte
});

console.log(`Executing the transaction will cost ${ethers.utils.formatEther(txCostPrice)} ETH`);

// initiating L2 transfer via L1 execute from zksync wallet
const someL2Contract = "0x19a5bfcbe15f98aa073b9f81b58466521479df8d";
const executeTx = await wallet.requestExecute({
  calldata,
  l2GasLimit: gasLimit,
  gasPerPubdataByte,
  contractAddress: someL2Contract,
  overrides: {
    gasPrice,
    value: txCostPrice,
  },
});

await executeTx.wait();