Cross Chain Tutorial Intro

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 the L1-governance folder.
cd L1-governance
  1. Initialize and set up the L1 project.
npx hardhat --init

Select the option to use hardhat-2 with typescript and mocha-ethers. Select yes to install the default dependencies.

Hardhat v3 is not fully supported for @matterlabs/hardhat-zksync.
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:
npm i -D typescript ts-node @openzeppelin/contracts @matterlabs/zksync-contracts@beta @nomicfoundation/hardhat-ethers @typechain/ethers-v6 @typechain/hardhat typechain ethers dotenv

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 the existing /test directory and any contracts that exist in /contracts.
  2. Create a new file called Governance.sol inside the empty contracts folder.
touch contracts/Governance.sol
  1. Copy/paste the code below into it.
L1-governance/contracts/Governance.sol
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;

import { IBridgehub, L2TransactionRequestDirect } from '@matterlabs/zksync-contracts/contracts/l1-contracts/bridgehub/IBridgehub.sol';

contract Governance {
  address public governor;

  constructor() {
    governor = msg.sender;
  }

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

    IBridgehub zksyncBridgeHub = IBridgehub(bridgeHubAddress);
    L2TransactionRequestDirect memory requestInput = L2TransactionRequestDirect(
      chainId,
      cost,
      contractAddr,
      0,
      data,
      gasLimit,
      gasPerPubdataByteLimit,
      new bytes[](0),
      msg.sender
    );
    zksyncBridgeHub.requestL2TransactionDirect{ value: msg.value }(requestInput);
  }
}

Made with ❤️ by the ZKsync Community