hardhat-zksync-upgradable
Description
The hardhat-zksync-upgradable
plugin is a Hardhat plugin that supports end-to-end pipelines for deploying and updating upgradable smart contracts.
Key features include:
- Support for three types of proxies: Transparent, UUPS, and Beacon
- Integration with OpenZeppelin's upgrade patterns
- Gas fee estimation for proxy deployments
- Automatic proxy admin management
- Support for complex constructor arguments
- Manifest file management for tracking deployments
The plugin is based on @openzeppelin/upgrades-core and provides a seamless integration with OpenZeppelin's upgrade patterns.
Resources:
Version Compatibility Warning
Ensure you are using the correct version of the plugin with ethers:- For plugin version <1.0.0:
- Compatible with ethers v5.
- For plugin version ≥1.0.0:
- Compatible with ethers v6 (⭐ Recommended)
OpenZeppelin Contracts Upgradable Compatibility Warning
Ensure you are using the correct version of the plugin with ethers:- For plugin version <1.6.0:
- Compatible with
@openzeppelin/contracts-upgradeable
v4
- Compatible with
- For plugin version ≥1.7.0:
- Compatible with
@openzeppelin/contracts-upgradeable
v5 (⭐ Recommended)
- Compatible with
Installation
Prerequisites
- Node.js version 18 or higher
- Hardhat version 2.16.0 or higher
Setup
Install the plugin and its dependencies:
yarn add -D @matterlabs/hardhat-zksync-upgradable @openzeppelin/upgrades-core @openzeppelin/contracts-upgradeable @openzeppelin/contracts
Import the package in your hardhat.config.ts
file:
import "@matterlabs/hardhat-zksync-upgradable";
Configuration
Configuration Options
Option | Type | Description | Required | Default |
---|---|---|---|---|
url | string | ZKsync Era node URL | Yes | - |
ethNetwork | string | Ethereum network name or URL | Yes | - |
zksync | boolean | Enable ZKsync Era network | Yes | - |
verifyURL | string | Verification endpoint URL | No | Network-specific default |
deployerAccounts | object | Default account indices per network | No | { default: 0 } |
accounts | string | { mnemonic: string } | Network accounts | No | - |
deployerAccount | number | Default account index for deployment | No | 0 |
Network Configuration
The plugin requires specific network configuration for both ZKsync Era and Ethereum networks. Here's a complete example:
import "@matterlabs/hardhat-zksync-solc";
import "@matterlabs/hardhat-zksync-deploy";
import "@matterlabs/hardhat-zksync-upgradable";
import { HardhatUserConfig } from "hardhat/config";
const config: HardhatUserConfig = {
zksolc: {
version: "latest",
settings: {},
},
defaultNetwork: "ZKsyncNetwork",
networks: {
// Ethereum network configuration
sepolia: {
url: "https://sepolia.infura.io/v3/<API_KEY>",
accounts: {
mnemonic: "your mnemonic here"
}
},
// ZKsync Era network configuration
ZKsyncNetwork: {
url: "http://localhost:3050",
ethNetwork: "sepolia",
zksync: true,
accounts: {
mnemonic: "your mnemonic here"
}
}
},
solidity: {
version: "0.8.19",
},
// Deployer account configuration for plugin tasks
deployerAccounts: {
zkTestnet: 1, // Use the second account for zkTestnet
default: 0 // Use the first account for other networks
}
};
export default config;
Additions
Added Tasks
The plugin adds the following tasks to Hardhat:
deploy-zksync:proxy
- Deploys proxy contracts (Transparent or UUPS)upgrade-zksync:proxy
- Upgrades proxy contractsdeploy-zksync:beacon
- Deploys beacon contracts and their proxiesupgrade-zksync:beacon
- Upgrades beacon contract implementations
Added HRE Extensions
The plugin adds the following extensions to the Hardhat Runtime Environment (HRE):
zksyncUpgrades and upgrades Objects
The plugin provides two objects for managing upgradeable contracts:
- zksyncUpgrades
- Specifically designed for ZKsync networks
- Provides a ZKsync-specific implementation of the upgrade interface
- Handles ZKsync-specific deployment and upgrade patterns
- Optimized for ZKsync network characteristics
- upgrades
- Supports OpenZeppelin's interface
- Automatically switches between ZKsync and non-ZKsync implementations
- Useful for projects that need to support both ZKsync and EVM networks
Both objects provide the following methods:
Method | Description | Network Support |
---|---|---|
deployProxy | Deploys a new proxy contract | Both |
upgradeProxy | Upgrades an existing proxy | Both |
deployBeacon | Deploys a new beacon contract | Both |
upgradeBeacon | Upgrades a beacon contract | Both |
deployBeaconProxy | Deploys a new beacon proxy | Both |
Example usage:
// Using zksyncUpgrades (specifically for ZKsync networks)
const proxy = await hre.zksyncUpgrades.deployProxy(Box, [42], {
initializer: "initialize",
});
// Using upgrades (OpenZeppelin interface)
const proxy = await hre.upgrades.deployProxy(Box, [42], {
initializer: "initialize",
});
zkUpgrades.estimation Object
The zkUpgrades.estimation
object provides methods for estimating gas fees:
Method | Description |
---|---|
estimateGasProxy | Estimates gas for proxy deployment |
estimateGasBeacon | Estimates gas for beacon deployment |
estimateGasBeaconProxy | Estimates gas for beacon proxy deployment |
Example usage:
const gasEstimate = await hre.zkUpgrades.estimation.estimateGasProxy(
deployer,
contract,
[]
);
Commands
deploy-zksync:proxy
Deploys a proxy contract (Transparent or UUPS).
yarn hardhat deploy-zksync:proxy --contract-name <contract name or FQN> \
[<constructor arguments>] \
[--constructor-args <javascript module name>] \
[--deployment-type <deployment type>] \
[--initializer <initialize method>] \
[--initial-owner <initial owner>] \
[--no-compile]
Example:
# Deploy with constructor arguments
yarn hardhat deploy-zksync:proxy --contract-name Box 42
# Deploy with complex constructor arguments
yarn hardhat deploy-zksync:proxy --contract-name ComplexContract --constructor-args args.js
# Deploy with custom initializer
yarn hardhat deploy-zksync:proxy --contract-name Box --initializer initializeBox
upgrade-zksync:proxy
Upgrades a proxy contract implementation.
yarn hardhat upgrade-zksync:proxy --contract-name <contract name or FQN> \
--proxy-address <proxy address> \
[--deployment-type <deployment type>] \
[--no-compile]
Example:
# Upgrade proxy implementation
yarn hardhat upgrade-zksync:proxy --contract-name BoxV2 --proxy-address 0x123...
deploy-zksync:beacon
Deploys a beacon contract and its proxy.
yarn hardhat deploy-zksync:beacon --contract-name <contract name or FQN> \
[<constructor arguments>] \
[--constructor-args <javascript module name>] \
[--deployment-type <deployment type>] \
[--initializer <initialize method>] \
[--initial-owner <initial owner>] \
[--no-compile]
Example:
# Deploy beacon with constructor arguments
yarn hardhat deploy-zksync:beacon --contract-name Token 1000000
upgrade-zksync:beacon
Upgrades a beacon contract implementation.
yarn hardhat upgrade-zksync:beacon --contract-name <contract name or FQN> \
--beacon-address <beacon address> \
[--deployment-type <deployment type>] \
[--no-compile]
Example:
# Upgrade beacon implementation
yarn hardhat upgrade-zksync:beacon --contract-name TokenV2 --beacon-address 0x456...
Command Options
Option | Description | Required | Default |
---|---|---|---|
--contract-name | Contract name or fully qualified name | Yes | - |
--proxy-address | Address of the proxy to upgrade | Yes (for upgrade) | - |
--beacon-address | Address of the beacon to upgrade | Yes (for beacon upgrade) | - |
--constructor-args | JavaScript module containing constructor arguments | No | - |
--deployment-type | Deployment type (create/create2/createAccount/create2Account) | No | create |
--initializer | Initializer function name | No | initialize |
--initial-owner | Initial contract owner address | No | wallet address |
--no-compile | Skip compilation step | No | false |
deployerAccount
configuration in your hardhat.config.ts
file,
or the account with index 0
if not configured.Usage
Proxy Patterns
The plugin supports three types of proxies, each with its own use case:
- Transparent Proxy
- Simple upgrade pattern with clear separation of admin/user functions
- Higher gas costs but simpler to understand and use
- Best for contracts with clear admin/user separation
- Example use case: Governance contracts, admin-controlled tokens
- UUPS Proxy
- More gas-efficient upgrade pattern
- Upgrade logic in the implementation contract
- Requires careful implementation of upgrade logic
- Example use case: Gas-optimized contracts, complex upgrade logic
- Beacon Proxy
- Allows multiple proxies to share the same implementation
- Centralized logic updates across multiple proxies
- More complex setup but efficient for multiple instances
- Example use case: Factory contracts, multiple token instances
Implementation Addresses and Manifest Files
When deploying proxy contracts, it's important to understand how implementation addresses are managed:
- Implementation Contract Reuse
- All interactions with your implementation contract go through the proxy
- Multiple proxy deployments for the same implementation contract share the same implementation
- This allows for gas optimization by reusing existing implementations
- Manifest File Management
- The plugin maintains a manifest file in the
.upgradable
folder of your project - Each network has its own manifest file
- The manifest file stores:
- Implementation contract addresses
- Proxy contract addresses
- Upgrade history
- Network-specific deployment information
- The plugin maintains a manifest file in the
Deployment Examples
Box Contract Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract Box is Initializable{
uint256 private value;
uint256 private secondValue;
uint256 private thirdValue;
function initialize(uint256 initValue) public initializer {
value = initValue;
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract BoxV2 is Initializable{
uint256 private value;
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
function initialize(uint256 initValue) public initializer {
value = initValue;
}
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
// Reads the last stored value and returns it with a prefix
function retrieve() public view returns (string memory) {
return string(abi.encodePacked("V2: ", uint2str(value)));
}
// Converts a uint to a string
function uint2str(uint _i) internal pure returns (string memory) {
if (_i == 0) {
return "0";
}
uint j = _i;
uint len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len;
while (_i != 0) {
k = k - 1;
uint8 temp = (48 + uint8(_i - (_i / 10) * 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}
}
UUPS Box Contract Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';
import '@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
contract BoxUups is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 private value;
uint256 private secondValue;
uint256 private thirdValue;
function initialize(uint256 initValue) public initializer {
value = initValue;
__Ownable_init(msg.sender);
__UUPSUpgradeable_init();
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
function _authorizeUpgrade(address) internal override onlyOwner {}
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
}
// V2 with additional functionality
pragma solidity ^0.8.16;
import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';
import '@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
contract BoxUupsV2 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 private value;
uint256 private secondValue;
uint256 private thirdValue;
function initialize(uint256 initValue) public initializer {
value = initValue;
}
// Reads the last stored value and returns it with a prefix
function retrieve() public view returns (string memory) {
return string(abi.encodePacked('V2: ', uint2str(value)));
}
// Converts a uint to a string
function uint2str(uint _i) internal pure returns (string memory) {
if (_i == 0) {
return '0';
}
uint j = _i;
uint len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len;
while (_i != 0) {
k = k - 1;
uint8 temp = (48 + uint8(_i - (_i / 10) * 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
function _authorizeUpgrade(address) internal override onlyOwner {}
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
}
Transparent Proxy
// mnemonic for local node rich wallet
const testMnemonic = "stuff slice staff easily soup parent arm payment cotton trade scatter struggle";
const zkWallet = Wallet.fromMnemonic(testMnemonic);
const deployer = new Deployer(hre, zkWallet);
const boxContract = await deployer.loadArtifact('Box');
// Deploy transparent proxy
const proxy = await hre.zkUpgrades.deployProxy(deployer.zkWallet, boxContract, [42], {
initializer: "initialize"
});
// Upgrade transparent proxy
const boxV2Contract = await deployer.loadArtifact('BoxV2');
await hre.zkUpgrades.upgradeProxy(deployer.zkWallet, await proxy.getAddress(), boxV2Contract);
UUPS Proxy
// mnemonic for local node rich wallet
const testMnemonic = "stuff slice staff easily soup parent arm payment cotton trade scatter struggle";
const zkWallet = Wallet.fromMnemonic(testMnemonic);
const deployer = new Deployer(hre, zkWallet);
const boxContract = await deployer.loadArtifact('BoxUups');
// Deploy transparent proxy
const proxy = await hre.zkUpgrades.deployProxy(deployer.zkWallet, boxContract, [42], {
initializer: "initialize"
});
// Upgrade transparent proxy
const boxV2Contract = await deployer.loadArtifact('BoxUupsV2');
await hre.zkUpgrades.upgradeProxy(deployer.zkWallet, await proxy.getAddress(), boxV2Contract);
Beacon Proxy
// mnemonic for local node rich wallet
const testMnemonic = "stuff slice staff easily soup parent arm payment cotton trade scatter struggle";
const zkWallet = Wallet.fromMnemonic(testMnemonic);
const deployer = new Deployer(hre, zkWallet);
const contractName = "Box";
const boxContract = await deployer.loadArtifact(contractName);
// Deploy beacon and beacon proxy
const beacon = await hre.zkUpgrades.deployBeacon(deployer.zkWallet, boxContract);
const proxy = await hre.zkUpgrades.deployBeaconProxy(deployer.zkWallet, beacon, boxContract, [42]);
// Upgrade transparent proxy
const boxV2Contract = await deployer.loadArtifact('BoxV2');
await hre.zkUpgrades.upgradeBeacon(deployer.zkWallet, await beacon.getAddress(), boxV2Contract);
Validation Notes
Gas Estimation
The plugin provides methods to estimate gas fees for proxy deployments:
// For Transparent/UUPS proxies
const totalGasEstimation = await hre.zkUpgrades.estimation.estimateGasProxy(
deployer,
contract,
[],
{ kind: "transparent" } // or "uups"
);
// For Beacon contracts
const totalGasEstimation = await hre.zkUpgrades.estimation.estimateGasBeacon(
deployer,
contract,
[]
);
// For Beacon proxies
const totalGasEstimation = await hre.zkUpgrades.estimation.estimateGasBeaconProxy(
deployer,
contract,
[]
);
Verification
hardhat-zksync-verify
plugin version >=0.1.8.The hardhat-zksync-upgradable
plugin supports comprehensive proxy verification, allowing you to verify all contracts deployed
during the proxy deployment process with a single command. This includes:
- Implementation contract
- Proxy contract
- Proxy admin contract (for Transparent proxies)
- Beacon contract (for Beacon proxies)
Setup
Import the verify plugin before the upgradable plugin in your hardhat.config.ts
:
// Import order matters - verify plugin must come first
import '@matterlabs/hardhat-zksync-verify';
import '@matterlabs/hardhat-zksync-upgradable';
Configure verification settings in your hardhat.config.ts
:
const config: HardhatUserConfig = {
// ... other config
verifyURL: "https://explorer.sepolia.era.zksync.dev/contract_verification", // Optional
};
Verification Command
To verify all contracts associated with a proxy deployment:
yarn hardhat verify <proxy address>
This single command will:
- Verify the implementation contract
- Verify the proxy contract
- Verify any associated contracts (admin, beacon)
Best Practices
- Testing
- Always test upgrades on testnet first
- Use local node for initial development
- Test all upgrade scenarios thoroughly
- Deployment Management
- Keep track of proxy addresses and implementation versions
- Document upgrade history
- Maintain a deployment manifest
- Gas Optimization
- Use gas estimation before deployment
- Consider using UUPS for gas-optimized contracts
- Test gas costs for different proxy patterns
- Security
- Verify contracts after deployment
- Follow OpenZeppelin's upgradeable contract guidelines
- Implement proper access control
Troubleshooting
Common Issues
- Version Compatibility
- Ensure correct ethers version (v5 or v6)
- Check OpenZeppelin Contracts Upgradable version compatibility
- Verify Hardhat version is 2.16.0 or higher
- Solution: Update dependencies to compatible versions
- Verification Failures
- Make sure
hardhat-zksync-verify
plugin is imported beforehardhat-zksync-upgradable
- Check network configuration and API keys
- Verify contract source code matches deployed bytecode
- Solution:
- Reorder plugin imports
- Verify network configuration
- Ensure source code is unchanged
- Make sure
- Initialization Errors
- Ensure contract implements
Initializable
- Check initializer function name matches configuration
- Verify constructor arguments are correct
- Solution:
- Add
Initializable
to contract inheritance - Check initializer name in configuration
- Validate constructor arguments
- Add
- Ensure contract implements
- Upgrade Validation Failures
- Follow OpenZeppelin's upgradeable contract guidelines
- Check storage layout compatibility
- Verify new implementation is compatible with proxy pattern
- Solution:
- Review upgrade guidelines
- Maintain storage layout
- Use compatible proxy pattern
- Gas Estimation Issues
- Network connection problems
- Invalid contract artifacts
- Incorrect deployment parameters
- Solution:
- Check network connectivity
- Verify contract compilation
- Validate deployment parameters
- Proxy Deployment Failures
- Insufficient funds
- Network issues
- Invalid contract bytecode
- Solution:
- Ensure sufficient funds
- Check network status
- Verify contract compilation
For more help, please refer to the GitHub issues.