hardhat-zksync-upgradable

Guide on using the hardhat-zksync-upgradable plugin.

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)
Examples are adopted for plugin version >=1.0.0

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
  • For plugin version ≥1.7.0:
    • Compatible with @openzeppelin/contracts-upgradeable v5 (⭐ Recommended)

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

OptionTypeDescriptionRequiredDefault
urlstringZKsync Era node URLYes-
ethNetworkstringEthereum network name or URLYes-
zksyncbooleanEnable ZKsync Era networkYes-
verifyURLstringVerification endpoint URLNoNetwork-specific default
deployerAccountsobjectDefault account indices per networkNo{ default: 0 }
accountsstring | { mnemonic: string }Network accountsNo-
deployerAccountnumberDefault account index for deploymentNo0

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 contracts
  • deploy-zksync:beacon - Deploys beacon contracts and their proxies
  • upgrade-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:

  1. 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
  2. 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:

MethodDescriptionNetwork Support
deployProxyDeploys a new proxy contractBoth
upgradeProxyUpgrades an existing proxyBoth
deployBeaconDeploys a new beacon contractBoth
upgradeBeaconUpgrades a beacon contractBoth
deployBeaconProxyDeploys a new beacon proxyBoth

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:

MethodDescription
estimateGasProxyEstimates gas for proxy deployment
estimateGasBeaconEstimates gas for beacon deployment
estimateGasBeaconProxyEstimates 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

OptionDescriptionRequiredDefault
--contract-nameContract name or fully qualified nameYes-
--proxy-addressAddress of the proxy to upgradeYes (for upgrade)-
--beacon-addressAddress of the beacon to upgradeYes (for beacon upgrade)-
--constructor-argsJavaScript module containing constructor argumentsNo-
--deployment-typeDeployment type (create/create2/createAccount/create2Account)Nocreate
--initializerInitializer function nameNoinitialize
--initial-ownerInitial contract owner addressNowallet address
--no-compileSkip compilation stepNofalse
Note: The account used for deployment will be the one specified by the 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:

  1. 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
  2. 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
  3. 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:

  1. 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
  2. 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
Note: The manifest file is network-specific, meaning you'll have different data for different networks. Make sure to keep track of these files in your version control system.

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

Important: The current version of the hardhat-zksync-upgradable plugin does NOT support all the validation checks. This means that it is the users responsibility to check if the new implementation they want to upgrade follows the predefined standards. At the time of writing, we are working on implementing those checks within the plugin itself, and the plan for subsequent releases is to support them natively.

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

Important: To use proxy verification functionality, you must use the 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:

  1. Verify the implementation contract
  2. Verify the proxy contract
  3. Verify any associated contracts (admin, beacon)
Note: The verification process will automatically detect the type of proxy (Transparent, UUPS, or Beacon) and verify all relevant contracts accordingly.

Best Practices

  1. Testing
    • Always test upgrades on testnet first
    • Use local node for initial development
    • Test all upgrade scenarios thoroughly
  2. Deployment Management
    • Keep track of proxy addresses and implementation versions
    • Document upgrade history
    • Maintain a deployment manifest
  3. Gas Optimization
    • Use gas estimation before deployment
    • Consider using UUPS for gas-optimized contracts
    • Test gas costs for different proxy patterns
  4. Security
    • Verify contracts after deployment
    • Follow OpenZeppelin's upgradeable contract guidelines
    • Implement proper access control

Troubleshooting

Common Issues

  1. 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
  2. Verification Failures
    • Make sure hardhat-zksync-verify plugin is imported before hardhat-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
  3. 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
  4. 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
  5. Gas Estimation Issues
    • Network connection problems
    • Invalid contract artifacts
    • Incorrect deployment parameters
    • Solution:
      • Check network connectivity
      • Verify contract compilation
      • Validate deployment parameters
  6. 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.


Made with ❤️ by the ZKsync Community