Migrating Hardhat project to ZKsync Era
Prerequisites
- Node.js version 18 or newer is required.
- To ensure compatibility with our plugins, make sure that Hardhat version 2.16.0 or newer is installed in your project.
Project setup
The @matterlabs/hardhat-zksync
plugin includes all the necessary tools to compile, test, deploy, and verify contracts on ZKsync.
- Install the
@matterlabs/hardhat-zksync
plugin with:npm i -D @matterlabs/hardhat-zksync
- Import the plugin in the
hardhat.config.ts
file. If your project has multiple plugins, import@matterlabs/hardhat-zksync
last:// Other Hardhat plugin imports above import "@matterlabs/hardhat-zksync";
- Remove conflicting plugin imports
@nomicfoundation/hardhat-ethers
:@matterlabs/hardhat-zksync
includeshardhat-ethers
extended with with support for ZKsync and non-ZKsync networks.@openzeppelin/hardhat-upgrades
:@matterlabs/hardhat-zksync
includeshardhat-upgrades
extended with support for ZKsync and non-ZKsync networks.@nomicfoundation/hardhat-toolbox
: this plugin includes@nomicfoundation/hardhat-ethers
which can cause conflicts. Import necessary components separately excluding@nomicfoundation/hardhat-ethers
.
// import "@nomicfoundation/hardhat-toolbox" // import "@nomicfoundation/hardhat-ethers" // import "@openzeppelin/hardhat-upgrades" import "@matterlabs/hardhat-zksync"
- Add the preferred ZKsync networks to the
hardhat.config.ts
file:networks: { zkSyncSepoliaTestnet: { url: "https://sepolia.era.zksync.dev", ethNetwork: "sepolia", zksync: true, verifyURL: "https://explorer.sepolia.era.zksync.dev/contract_verification", }, zkSyncMainnet: { url: "https://mainnet.era.zksync.io", ethNetwork: "mainnet", zksync: true, verifyURL: "https://zksync2-mainnet-explorer.zksync.io/contract_verification", }, dockerizedNode: { url: "http://localhost:3050", ethNetwork: "http://localhost:8545", zksync: true, }, inMemoryNode: { url: "http://127.0.0.1:8011", ethNetwork: "localhost", zksync: true, }, // Other networks }
zksync:true
flag to the hardhat
network.Compilation
ZKsync Era (as well as other chains built with ZK Stack) is operated by the EraVM,
which executes a specific bytecode that differs from the EVM.
This bytecode is generated by the zksolc
(for Solidity contracts)
and zkvyper
(for Vyper contracts) compilers.
To compile your contracts with these compilers, follow these steps:
- Run the compilation task targeting one of the ZKsync networks, which contain
zksync: true
:npx hardhat compile --network zkSyncSepoliaTestnet
- The following output indicates the contracts are being compiled with the
zksolc
compiler:Compiling contracts for ZKsync Era with zksolc v1.5.1 and zkvm-solc v0.8.17-1.0.1 Compiling 42 Solidity files
- The compiler generates the
/artifacts-zk
and/cache-zk
folders containing the smart contract correspondent artifacts, which follow the same structure as the ones generated by thesolc
compiler.
Compiler settings
You can modify different compiler settings in the zksolc
or zkvyper
property inside the hardhat.config.ts
file.
- Check the available
zksolc
settings here. - Check the available
zkvyper
settings here.
Non-inline Libraries
Deploying non-inline libraries on ZKsync differs from Ethereum.
On Ethereum, non-inlineable libraries must be deployed beforehand, and then referenced in the deployment transaction of the contract that imports them:
const firstLibrary = await hre.ethers.deployContract("LibraryA");
await firstLibrary.waitForDeployment();
const firstLibraryAddress = await firstLibrary.getAddress()
const secondLibrary = await hre.ethers.deployContract("LibraryB");
await secondLibrary.waitForDeployment();
const secondLibraryAddress = await l2.getAddress();
const mainContract = await hre.ethers.deployContract("MainContract",{
libraries:{
LibraryA:firstLibraryAddress,
LibraryB:secondLibraryAddress
}
});
await mainContract.waitForDeployment();
On ZKsync, if your project contains non-inlineable libraries, the compilation command will throw an error.
To automatically deploy all non-inlineable libraries, follow these steps:
- Configure a deployer account in the ZKsync network you want to deploy by adding the
accounts:[]
property. - Run the following command to deploy the libraries and auto-generate the libraries configuration in the
hardhat.config.ts
file:npx hardhat deploy-zksync:libraries
The command will add the libraries and their addresses to the hardhat.config.ts
file.
- Now you can compile the main contract that imports the libraries and deploy the contract without the need to reference the libraries:
const mainContract = await hre.ethers.deployContract("MainContract")
await mainContract.waitForDeployment();
Troubleshoting
Use of unsupported opcodes like SELFDESTRUCT or EXTCODECOPY. The compiler will throw an error if any unsupported opcodes is used in one of the contracts. See differences with EVM opcodes in EVM instructions.
Testing
ZKSync provides different EraVM node implementations to test smart contracts locally:
- In-Memory Node: fast L2 node with non-persistent state.
- Dockerized setup: L1 and L2 nodes with persistent state but slower performance.
Unless your project contains L1-L2 features, testing with the In-Memory Node is recommended, which is included in the @matterlabs/hardhat-zksync
plugin.
1.0.12
, hardhat-network-helpers
introduced support for both In-Memory Node and Dockerized setups, allowing methods
such as loadFixture
to be utilized in test files.You can read more about each node in the Testing section of the docs.
Running unit tests on In-Memory Node
To run tests using In-Memory Node, follow these steps:
- Add the
zksync:true
flag to thehardhat
network in thehardhat.config.ts
file to override Hardhat's default node with ZKsync In-Memory Node. - Run the test task with
npx hardhat test --network hardhat
(or makehardhat
the default network).
You can find more info about testing with the In-Memory-Node in Hardhat-ZKsync node.
Running tests on Dockerized setup
To run tests on the Dockerized local setup, follow these steps:
- Run
npx zksync-cli dev config
and select the “Dockerized node” option. - Run
npx zksync-cli dev start
to start the L1 and L2 nodes. - Add the Dockerized nodes to the list of networks in the
hardhat.config.ts
file:networks: { dockerizedNode: { url: "http://localhost:3050", ethNetwork: "http://localhost:8545", zksync: true, }, // Other networks }
- Make sure the providers in your test files target the correct url.
- Run the test task with
npx hardhat test --network dockerizedNode
.
Deployment
Smart contract deployment on ZKsync Era (and chains built with ZK Stack) differ from Ethereum
as they are handled by the ContractDeployer
system contract
(see Ethereum differences).
There are different approaches for contract deployment:
Deployment with hardhat-ethers
The @matterlabs/hardhat-zksync
includes @matterlabs/hardhat-zksync-ethers
, a package that extends @nomiclabs/hardhat-ethers
with all the necessary helper methods to deploy contracts on both ZKsync and EVM networks. The injected hre.ethers
object provides methods like deployContract
, getContractFactory
or getContractAt
so deployment scripts work out of the box.
@nomiclabs/hardhat-ethers
and @matterlabs/hardhat-zksync-ethers
make sure that only the latter
(or the wrapper plugin @matterlabs/hardhat-zksync
) is imported in the hardhat.config.ts
file.See below examples for hardhat-ethers
and hardhat-zksync-ethers
:
const greeter = await hre.ethers.deployContract('Greeter', ['Hi there!']);
await greeter.waitForDeployment();
When a custom deployment is needed, use ContractFactory
.
const GreeterFactory = await hre.ethers.getContractFactory('Greeter');
const greeterContract = GreeterFactory.deploy(); // if any, pass constructor arguments in deploy arguments
await greeter.waitForDeployment();
Deployment with hardhat-deploy
The newest versions of the hardhat-deploy
plugin (beginning with 0.11.26
) now support ZKsync
deployments out of the box. This means you don't need to modify your deployment scripts and methods like getNamedAccounts
can be used on ZKsync.
Deployment with hardhat-viem
To be included.
Deployment with Hardhat ignition
Hardhat ignition scripts do not support deployments to ZKsync Era yet. Please use other options like hardhat-deploy or hardhat-ethers
Deploying proxy contracts
The @matterlabs/hardhat-zksync
includes @matterlabs/hardhat-zksync-upgradable
,
which extends @openzeppelin/hardhat-upgrades
with all the necessary methods to deploy and upgrade proxy contracts on ZKsync and EVM networks.
The injected hre.upgrades
object provides methods like deployProxy
or deployBeacon
so deployment scripts work out of the box.
@openzeppelin/hardhat-upgrades
and @matterlabs/hardhat-zksync-upgradable
make sure that only the latter
(or the wrapper plugin @matterlabs/hardhat-zksync
) is imported in the hardhat.config.ts
file.See examples below:
Transparent proxy (deployment)
const constructorArguments = [...];
const initializerFunctionName = 'initialize'
const boxFactory = await hre.ethers.getContractFactory("Box");
const box = await hre.upgrades.deployProxy(boxFactory, constructorArguments, {
initializer: initializerFunctionName,
});
await box.waitForDeployment();
Transparent proxy (upgrade)
const constructorArguments = [...];
const upgradableProxyAddress = "UPGRADEABLE_PROXY_ADDRESS";
const boxV2ContractFactory = await hre.ethers.getContractFactory("BoxV2");
const boxV2 = await hre.upgrades.upgradeProxy(upgradableProxyAddress,boxV2ContractFactory);
await boxV2.waitForDeployment();
Beacon proxy (deployment)
const constructorArguments = [...];
const boxFactory = await hre.ethers.getContractFactory("Box");
const box = await hre.upgrades.deployBeacon(boxFactory, constructorArguments);
await box.waitForDeployment();
Beacon proxy (upgrade)
const upgradableProxyAddress = "UPGRADEABLE_PROXY_ADDRESS";
const boxV2ContractFactory = await hre.ethers.getContractFactory("BoxV2");
const boxV2 = await hre.upgrades.upgradeBeacon(upgradableProxyAddress, boxV2ContractFactory);
await boxV2.waitForDeployment();
UUPS proxy (deployment)
const constructorArguments = [...];
const initializerFunctionName = 'initialize'
const boxFactory = await hre.ethers.getContractFactory("Box");
const box = await hre.upgrades.deployProxy(boxFactory, constructorArguments, {
initializer: initializerFunctionName,
});
await box.waitForDeployment();
UUPS proxy (upgrade)
const constructorArguments = [...];
const upgradableProxyAddress = "UPGRADEABLE_PROXY_ADDRESS";
const boxV2ContractFactory = await hre.ethers.getContractFactory("BoxV2");
const boxV2 = await hre.upgrades.upgradeProxy(upgradableProxyAddress,boxV2ContractFactory);
await boxV2.waitForDeployment();
Custom deployment scripts with hardhat-zksync
Additionally, you can write custom deployment scripts for ZKsync leveraging the hre.deployer
object which is injected automatically by @matterlabs/hardhat-zksync-deploy
.
The Deployer
class provides helper methods to deploy smart contracts to ZKsync. Learn more on hardhat-zksync-deploy
methods.
Troubleshoting
- Contract size too large: if the size of the generated bytecode is too large and can not be deployed, try compiling
the contract with the
mode: 3
flag in thehardhat.config.ts
file to optimize the bytecode size on compilation. Learn more onhardhat-zksync-solc
configuration.
Smart contract verification
There are no differences in the verification of smart contracts on ZKsync.
By installing @matterlabs/hardhat-zksync
, a verification plugin is provided.
You will have to add a verifyURL
on the ZKsync networks in the hardhat.config.ts
file:
zkSyncSepoliaTestnet: {
url: "https://sepolia.era.zksync.dev",
ethNetwork: "sepolia",
zksync: true,
verifyURL: 'https://explorer.sepolia.era.zksync.dev/contract_verification'
},
zkSyncMainnet: {
url: "https://mainnet.era.zksync.io",
ethNetwork: "mainnet",
zksync: true,
verifyURL: "https://zksync2-mainnet-explorer.zksync.io/contract_verification",
},
You can run the verification task programmatically inside a script as follows:
const verificationId = await hre.run("verify:verify", {
address: contractAddress,
contract: contractFullyQualifedName,
constructorArguments: [...]
});
Alternatively you can execute the verification task from your terminal:
npx hardhat verify --network testnet 0x7cf08341524AAF292255F3ecD435f8EE1a910AbF "Hi there!"
Find more info in hardhat-zksync-verify
.
Scripting
Most scripts will work out of the box as interacting with smart contracts deployed on ZKsync is exactly the same as on any EVM chain.
For ZKsync-specific features like native account abstraction or paymaster transactions, there are plugins or extensions for the most popular libraries:
- Ethers: zksync-ethers.
- Web3.js: web3-plugin-zksync.
- Viem: ZKsync extension.
For other programming languages, please refer to the SDK documentation](https://sdk.zksync.io/).
Multichain projects
The following example project can be used as a reference to target both EVM and ZKsync chains.
Here are some recommendations for projects that target multiple chains:
- Add the desired ZKsync networks with the
zksync:true
flag to thehardhat.config.ts
. - Make sure to run the compilation task with the
--network
flag when targeting ZKsync networks to use the custom compiler. - When targeting ZKsync chains, the
@matterlabs/hardhat-zksync
plugin overrides the following plugins:@nomiclabs/hardhat-ethers
,@openzeppelin/hardhat-upgrades
. - To avoid typescript collision errors between
@nomiclabs/hardhat-ethers
and@openzeppelin/hardhat-upgrades
with@matterlabs/hardhat-zksync
make sure that only the latter is imported in thehardhat.config.ts
file. - Your deployment scripts should not require any changes if you're using
hardhat-deploy
orhardhat-ethers
. - If you have a separate directory for ZKsync deployment scripts, you can indicate the custom
deployment folder for the ZKsync network using the
deployPaths
property:
const config: HardhatUserConfig = {
networks: {
zksyncTestnet: {
url: "https://sepolia.era.zksync.dev",
ethNetwork: "sepolia",
zksync: true,
// ADDITION
deployPaths: "deploy-zksync", //single deployment directory
deployPaths: ["deploy", "deploy-zksync"], //multiple deployment directories
},
},
};
Support
If you're having issues migrating a Hardhat project to ZKsync, please reach out to us by creating a GitHub discussion.