L1 governance contract

Build and deploy a smart contract in L1 and send transactions that update the state of a contract in ZKsync.

In this tutorial we'll deploy a simple smart contract in ZKsync. Then we'll deploy another smart contract in L1 and send transactions that update the state of the contract in ZKsync. We'll use the Mailbox

What you'll learn:

  • How to deploy smart contracts in L1 and L2.
  • How to send transactions in L1 that interact with contracts in L2.


  • - zksync-cli
  • - zksync-ethers
  • - Hardhat
smart contractsL1-L2tutorial
Last Updated: May 09, 2024

This tutorial shows you how to implement communication between L1 and L2 with the following example:

  • A Governance Solidity smart contract is deployed on layer 1. This contract has a function that sends a transaction to ZKsync layer 2.
  • A Counter Solidity smart contract is deployed on ZKsync layer 2. This contract stores a number that is incremented by calling the increment method. The Governance contract on layer 1 calls this function.


  • Make sure your machine satisfies the system requirements.
  • You are already familiar with deploying smart contracts on ZKsync Era. If not, please refer to the first section of the quickstart tutorial.
  • You already have some experience working with Ethereum.
  • A wallet with sufficient Sepolia ETH on Ethereum and ZKsync Sepolia Testnet to pay for deploying smart contracts. You can get Sepolia ETH from the network faucets.
    • Get testnet ETH for ZKsync Era using bridges to bridge funds to ZKsync.
  • You know how to get your private key from your MetaMask wallet.
Skip the hassle for test ETH by using zksync-cli for local testing. Simply execute npx zksync-cli dev start to initialize a local ZKsync development environment, which includes local Ethereum and ZKsync nodes. This method allows you to test contracts without requesting external testnet funds. Explore more in the zksync-cli documentation.

Complete Project

Download the complete project on GitHub.

Project Setup

Open a terminal window, create a new folder for the project tutorial, e.g. mkdir cross-chain-tutorial, and cd into the folder.

Now create separate folders to store contracts and scripts on L1 and L2. For now we will start with L1-governance folder.

mkdir L1-governance
The L1-governance code is a default Hardhat project used to deploy a contract on L1. The L2-counter code includes all ZKsync dependencies and configurations for L2.

L1 Governance

  1. cd into L1-governance.
  2. Run the following to initialise and set up the L1 project:
npx hardhat

Select the option Create a Typescript project and accept the defaults for everything else.

To interact with the ZKsync bridge contract using Solidity, you need the ZKsync contract interface. There are two ways to get it:
  • Import it from the @matterlabs/zksync-contracts npm package (preferred).
  • Download it from the contracts repo.
  1. Install the following dependencies:

Make sure you use actual node (lts version) and actual npm version

npm i -D typescript ts-node @openzeppelin/contracts @matterlabs/zksync-contracts @nomicfoundation/hardhat-ethers @typechain/ethers-v6 @typechain/hardhat typechain ethers

Create L1 Governance Contract

Make sure you're still in the L1-governance folder.

The following Solidity code defines the Governance smart contract.

The constructor sets the contract creator as the single governor. The callZkSync function calls a transaction on L2 which can only be called by the governor.

  1. Remove existing /test directory and any contracts that exist in /contracts.
  2. cd into the contracts/ folder.
  3. Create a file called Governance.sol and copy/paste the code below into it.
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;

import "@matterlabs/zksync-contracts/l1/contracts/zksync/interfaces/IZkSync.sol";

contract Governance {
    address public governor;

    constructor() {
        governor = msg.sender;

    function callZkSync(
        address zkSyncAddress,
        address contractAddr,
        bytes memory data,
        uint256 gasLimit,
        uint256 gasPerPubdataByteLimit
    ) external payable {
        require(msg.sender == governor, "Only governor is allowed");

        IZkSync zksync = IZkSync(zkSyncAddress);
        zksync.requestL2Transaction{value: msg.value}(contractAddr, 0,
            data, gasLimit, gasPerPubdataByteLimit, new bytes[](0), msg.sender);

Deploy L1 Governance Contract

  1. Create the file L1-Governance/sepolia.json and copy/paste the code below, filling in the relevant values. Find node provider urls here. You have to connect your wallet to the network and add the network to the wallet in advance.
      "nodeUrl": "<SEPOLIA NODE URL>",
      "deployerPrivateKey": "<YOUR PRIVATE KEY>"
  2. Replace the code in hardhat.config.ts with the following:
    import "@nomicfoundation/hardhat-ethers";
    import { HardhatUserConfig } from "hardhat/config";
    // import file with Sepolia params
    const sepolia = require("./sepolia.json");
    const config: HardhatUserConfig = {
      solidity: {
        version: "0.8.20",
      networks: {
        // Sepolia network
        sepolia: {
          url: sepolia.nodeUrl,
          accounts: [sepolia.deployerPrivateKey],
    export default config;
  3. Navigate to the scripts folder and copy/paste the following code into the deploy.ts file (removing any previous code):
    // We require the Hardhat Runtime Environment explicitly here. This is optional
    // but useful for running the script in a standalone fashion through `node <script>`.
    // When running the script with `npx hardhat run <script>` you'll find the Hardhat
    // Runtime Environment's members available in the global scope.
    import { ethers } from "hardhat";
    async function main() {
      // We get the contract to deploy
      const Governance = await ethers.getContractFactory("Governance");
      const contract = await Governance.deploy();
      const receipt = await contract.deploymentTransaction()?.wait();
      console.log(`Governance contract was successfully deployed at ${receipt?.contractAddress}`);
    // We recommend always using this async/await pattern to properly handle errors.
    main().catch((error) => {
      process.exitCode = 1;
  4. From the L1-governance folder root, compile and deploy the contract:
    # compile contract
    npx hardhat compile
    # deploy contract
    npx hardhat run --network sepolia ./scripts/deploy.ts

    You should see the following output:
    Governance contract was successfully deployed at 0xf28Df77fa8ff56cA3084bd11c1CAF5033A7b8C4A

    Save the address to use in a later step.

Made with ❤️ by the ZKsync Community