Upgradability

Learn to make smart contracts upgradeable within the ZKsync ecosystem.

In this fourth installment for ZKsync 101, we embark on a journey through contract upgradability, an important aspect for maintaining and enhancing smart contracts over time. This guide will lead you through the strategies and practices for making the CrowdfundingCampaign contract upgradeable.

Harness advanced techniques for contract upgradability in ZKsync.

Implement upgradeable patterns for the CrowdfundingCampaign to ensure long-term adaptability and improvement.

Leverage tools and best practices in ZKsync to facilitate seamless contract upgrades.

Begin to understand smart contract evolution and empower your ZKsync applications with the flexibility of upgradability.

Select your preferred upgrade mechanism:

What is a transparent upgradeable proxy contract?

Transparent upgradeable contracts utilize the proxy pattern to facilitate post-deployment logic updates while preventing accidental function collisions. They consist of:

  1. Proxy Contract: Manages storage, balance, and delegates calls to the logic contract, except for those by the admin, ensuring clear separation between user and administrative interactions.
  2. Logic Contract: Houses the actual business logic, upgradeable by swapping out for new versions.
  3. Admin Address: Holds the rights to upgrade the logic contract, with its commands executed exclusively by the proxy to prevent unintended logic execution.

This setup ensures only non-administrative calls reach the logic contract, allowing for safe and seamless upgrades. By switching the logic contract to a newer version while keeping the original proxy intact, the contract's state and balance are preserved. This facilitates improvements or bug fixes without changing the proxy, maintaining a consistent user interface.


Run the following command in your terminal to initialize the project.

npx zksync-cli@latest create --template qs-upgrade contract-upgrade-quickstart
cd contract-upgrade-quickstart
If you encounter an error while installing project dependencies using NPM as your package manager, try running npm install --force.

Set up your wallet

Deploying contracts on the ZKsync Sepolia Testnet requires having testnet ETH. If you're working within the local development environment, you can utilize pre-configured rich wallets and skip this step. For testnet deployments, you should have your wallet funded from the previous step.


Adapt CrowdfundingCampaign.sol contract for upgradability

To adapt our CrowdfundingCampaign.sol contract for upgradability, we're transitioning to a proxy pattern. This approach separates the contract's logic (which can be upgraded) from its persistent state (stored in the proxy).

Refactoring for Proxy Compatibility

In the contracts/ directory you'll observe the refactored the CrowdfundingCampaign contract which initializes state variables through an initialize function instead of the constructor, in line with the Transparent Proxy pattern.

Updated Contract Structure:

CrowdfundingCampaign.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/utils/Initializable.sol";

contract CrowdfundingCampaign is Initializable {
    address public owner;
    uint256 public fundingGoal;
    mapping(address => uint256) public contributions;

    event ContributionReceived(address contributor, uint256 amount);
    event GoalReached(uint256 totalFundsRaised);

    // Remove constructor in favour of initialize method
    function initialize(uint256 _fundingGoal) public initializer {
        owner = msg.sender;
        fundingGoal = _fundingGoal;
    }

    function contribute() public payable {
        // contribution logic remains the same
    }

    function withdrawFunds() public {
        // withdrawFunds logic remains the same
    }

    function getTotalFundsRaised() public view returns (uint256) {
        // getTotalFundsRaised remains the same
    }

    function getFundingGoal() public view returns (uint256) {
        // getFundingGoal remains the same
    }
}

Key Modifications:

  • Initializable: Inherits from OpenZeppelin's Initializable to ensure the initialize function can only be called once, similar to a constructor.
  • Initialize Function: Replaces the constructor for setting initial state, facilitating upgrades through new logic contracts.
  • Proxy Pattern: Utilizes a proxy contract to delegate calls to this logic contract, allowing for future upgrades without losing the contract's state.

This restructuring prepares the CrowdfundingCampaign contract for upgradability.


Deploy the CrowdfundingCampaign contract

Now that the CrowdfundingCampaign contract is adapted for contract upgradability, let's proceed to deploy the contract so we may upgrade it in later steps. Since we've made changes to our contract we will need to re-compile.

To compile the contracts in the project, run the following command:

npm run compile

Upon successful compilation, you'll receive output detailing the zksolc and solc versions used during compiling and the number of Solidity files compiled.

Compiling contracts for ZKsync Era with zksolc v1.4.0 and solc v0.8.17
Compiling 3 Solidity file
Successfully compiled 3 Solidity file

The compiled artifacts will be located in the /artifacts-zk folder.

The deployment script is located at /deploy/deployTransparentProxy.ts.

deployTransparentProxy.ts
import { getWallet } from "./utils";
import { Deployer } from '@matterlabs/hardhat-zksync';
import { ethers } from "ethers";
import { HardhatRuntimeEnvironment } from "hardhat/types";

export default async function (hre: HardhatRuntimeEnvironment) {
    const wallet = getWallet();
    const deployer = new Deployer(hre, wallet);

    const contractArtifact = await deployer.loadArtifact("CrowdfundingCampaign");
    const fundingGoalInWei = ethers.parseEther('0.1').toString();
    // Deploy the contract using a transparent proxy
    const crowdfunding = await hre.zkUpgrades.deployProxy(
        getWallet(),
        contractArtifact,
        [fundingGoalInWei],
        { initializer: 'initialize' }
    );

    await crowdfunding.waitForDeployment();
}

Key Components:

  • hre.zkUpgrades.deployProxy: The method call to deploy the CrowdfundingCampaign contract via a transparent proxy, leveraging Hardhat's runtime environment for ZKsync upgrades. This ensures the deployed contract can be upgraded in the future without losing its state or funds.
  • initializer: Specifies the initialization method of the contract, initialize in this case, which is required for setting up the proxy's state upon deployment.

Execute the deployment command corresponding to your package manager. The default command deploys to the configured network in your Hardhat setup. For local deployment, append --network inMemoryNode to deploy to the local in-memory node running.

npx hardhat deploy-zksync --script deployTransparentProxy.ts
# To deploy the contract on local in-memory node:
# npx hardhat deploy-zksync --script deployTransparentProxy.ts --network inMemoryNode

Upon successful deployment, you'll receive output detailing the deployment process, including the contract addresses of the implementation contract, the admin contract, and the transparent proxy contract.

Implementation contract was deployed to 0xE3F814fa915A75bA47230537726C99f6517Da58e
Admin was deployed to 0x05198D9f93cBDfa3e332776019115512d8e0c809
Transparent proxy was deployed to 0x68E8533acE01019CB8D07Eca822369D5De71b74D

Upgrade the CrowdfundingCampaign Contract

With our initial setup deployed, we're ready to update our CrowdfundingCampaign.sol contract by incorporating a deadline for contributions. This addition not only brings a new layer of functionality but also introduces the concept of time-based conditions through a modifier.

Current Contract Overview:

The existing version of our contract allows for open-ended contributions towards a funding goal, without any time constraints.

Proposed Upgrade:

We're introducing a deadline variable, initialized at contract deployment, to establish a clear timeframe for accepting contributions. The withinDeadline modifier will then enforce this constraint, ensuring contributions are made within the allowed period.

Enhanced Contract:

The upgraded contract, CrowdfundingCampaignV2.sol, located in the /contracts directory, incorporates these changes:

  • Deadline Variable: A new state variable deadline defines the campaign's end time, enhancing the contract with time-based logic.
  • Initialization Logic: An additional initialization method, initializeV2, sets the deadline based on a duration provided during the upgrade. This function ensures that the upgrade is backward-compatible and maintains the contract's integrity.
  • Contribution Logic with Deadline: The contribute method now includes a withinDeadline modifier, ensuring all contributions are made within the set timeframe.
  • Deadline Enforcement: The withinDeadline modifier checks the current time against the deadline, safeguarding the contract from late contributions.

Deadline Extension Capability:

To provide flexibility, a new function allows the owner to extend the deadline, offering adaptability to changing campaign needs.

CrowdfundingCampaignV2.sol
function extendDeadline(uint256 _newDuration) public {
    require(msg.sender == owner, "Only the owner can extend the deadline");
    deadline = block.timestamp + _newDuration;
}

This upgrade not only introduces the element of time to the campaign but also exemplifies the use of modifiers for enforcing contract conditions.

Compile contract

Smart contracts deployed to ZKsync must be compiled using our custom compiler. zksolc is the compiler used for Solidity.

To compile the contracts in a project, run the following command:

npm run compile

Upon successful compilation, you'll receive output detailing the zksolc and solc versions used during compiling and the number of Solidity files compiled.

Compiling contracts for ZKsync Era with zksolc v1.4.0 and solc v0.8.17
Compiling 29 Solidity file
Successfully compiled 29 Solidity file

The compiled artifacts will be located in the /artifacts-zk folder.

Upgrade to CrowdfundingCampaignV2

This section guides you through upgrading the CrowdfundingCampaign contract to its second version, CrowdfundingCampaignV2. Review the upgradeCrowdfundingCampaign.ts script located within the deploy/upgrade-scripts directory to begin.

Replace YOUR_PROXY_ADDRESS_HERE with the actual address of your deployed Transparent Proxy from the previous deployment step.

upgradeCrowdfundingCampaign.ts
import { getWallet } from "../utils";
import { Deployer } from '@matterlabs/hardhat-zksync';
import { HardhatRuntimeEnvironment } from "hardhat/types";

export default async function (hre: HardhatRuntimeEnvironment) {
    const wallet = getWallet();
    const deployer = new Deployer(hre, wallet);

    // Placeholder for the deployed proxy address
    const proxyAddress = 'YOUR_PROXY_ADDRESS_HERE';

    const contractV2Artifact = await deployer.loadArtifact('CrowdfundingCampaignV2');

    // Upgrade the proxy to V2
    const upgradedContract = await hre.zkUpgrades.upgradeProxy(deployer.zkWallet, proxyAddress, contractV2Artifact);

    console.log('Successfully upgraded crowdfundingCampaign to crowdfundingCampaignV2');

    upgradedContract.connect(deployer.zkWallet);
    // wait some time before the next call
    await new Promise((resolve) => setTimeout(resolve, 2000));

    // Initialize V2 with a new campaign duration
    const durationInSeconds = 30 * 24 * 60 * 60; // For example, setting a 30-day duration
    const initTx = await upgradedContract.initializeV2(durationInSeconds);
    const receipt = await initTx.wait();

    console.log(`CrowdfundingCampaignV2 initialized. Transaction Hash: ${receipt.hash}`);
}

Key Components:

  • upgradeProxy: A critical method from the hre.zkUpgrades module that performs the contract upgrade. It takes the wallet, the proxy address, and the new contract artifact as arguments to transition the proxy to use the CrowdfundingCampaignV2 logic.
  • initializeV2: Post-upgrade, this function is invoked to initialize the new variables or logic introduced in CrowdfundingCampaignV2. In this example, it sets a new campaign duration, illustrating how contract upgrades can add functionalities without losing the existing state or funds.

Execute the command corresponding to your package manager:

npx hardhat deploy-zksync --script upgrade-scripts/upgradeCrowdfundingCampaign.ts

Upon successful deployment, you'll receive output detailing the upgrade process, including the contract address, and transaction hash:

Contract successfully upgraded to 0x58BD5adb462CF087E5838d53aE38A3Fe0EAf7A31 with tx 0xe30c017c52376507ab55bb51bc27eb300832dc46b8b9ac14549d2f9014cee97e
Successfully upgraded crowdfundingCampaign to crowdfundingCampaignV2
CrowdfundingCampaignV2 initialized! 0x5adfe360187195d98d3603a82a20ffe7304cd4dec030d1bdf456fa1690879668
Fundraising goal: 100000000000000000

Verify upgradable contracts

For the verification of our upgradable contracts, it's essential to utilize the proxy address that was specified in our upgrade script.

To proceed with verification, execute the following command:

Replace <PROXY_ADDRESS> with the actual proxy address from your deployment.

npx hardhat verify <PROXY-ADDRESS>

Upon successful verification, you'll receive output detailing the verification process:

Verifying implementation: 0x58BD5adb462CF087E5838d53aE38A3Fe0EAf7A31
Your verification ID is: 10543
Contract successfully verified on ZKsync block explorer!
Verifying proxy: 0x68E8533acE01019CB8D07Eca822369D5De71b74D
Your verification ID is: 10544
Contract successfully verified on ZKsync block explorer!
Verifying proxy admin: 0x05198D9f93cBDfa3e332776019115512d8e0c809
Your verification ID is: 10545
Contract successfully verified on ZKsync block explorer!

🎉 Congratulations! The CrowdfundingCampaignV2 contract has been upgraded and verified!

Coming soon!

What is a beacon proxy upgradeable contract?

Beacon Proxy Upgradeable Contracts leverage a beacon to manage upgrades, allowing for centralized logic updates across multiple proxies. The structure includes:

  1. Beacon Contract: Acts as the central point holding the address of the current logic contract. It enables updating the logic for all associated proxies through a single transaction.
  2. Proxy Contracts: These lightweight contracts delegate calls to the logic contract address provided by the beacon, maintaining their own state and balance.
  3. Logic Contract: Contains the executable business logic, which can be updated by changing the beacon's reference without altering individual proxies.
  4. Admin Address: Authorized to update the logic contract address in the beacon, ensuring controlled and secure upgrades.

This arrangement allows multiple proxy contracts to be upgraded simultaneously by updating the logic contract address in the beacon, streamlining the upgrade process. It preserves the state and balance of each proxy contract, offering an efficient way to roll out new features or fixes while maintaining a uniform interface for users.


Run the following command in your terminal to initialize the project.

npx zksync-cli@latest create --template qs-upgrade contract-upgrade-quickstart
cd contract-upgrade-quickstart
If you encounter an error while installing project dependencies using NPM as your package manager, try running npm install --force.

Set up your wallet

Deploying contracts on the ZKsync Sepolia Testnet requires having testnet ETH. If you're working within the local development environment, you can utilize pre-configured rich wallets and skip this step. For testnet deployments, you should have your wallet funded from the previous step.


Adapt CrowdfundingCampaign.sol contract for upgradability

To adapt our CrowdfundingCampaign.sol contract for upgradability, we are transitioning to a proxy pattern. This approach separates the contract's logic (which can be upgraded) from its persistent state (stored in the proxy).

In the contracts/ directory you'll observe the refactored the CrowdfundingCampaign contract which initializes state variables through an initialize function instead of the constructor, in line with the proxy pattern.

Updated Contract Structure:

CrowdfundingCampaign.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/utils/Initializable.sol";

contract CrowdfundingCampaign is Initializable {
    address public owner;
    uint256 public fundingGoal;
    mapping(address => uint256) public contributions;

    event ContributionReceived(address contributor, uint256 amount);
    event GoalReached(uint256 totalFundsRaised);

    // Remove constructor in favour of initialize method
    function initialize(uint256 _fundingGoal) public initializer {
        owner = msg.sender;
        fundingGoal = _fundingGoal;
    }

    function contribute() public payable {
        // contribution logic remains the same
    }

    function withdrawFunds() public {
        // withdrawFunds logic remains the same
    }

    function getTotalFundsRaised() public view returns (uint256) {
        // getTotalFundsRaised remains the same
    }

    function getFundingGoal() public view returns (uint256) {
        // getFundingGoal remains the same
    }
}

Key Modifications:

  • Initializable: Inherits from OpenZeppelin's Initializable to ensure the initialize function can only be called once, similar to a constructor.
  • Initialize Function: Replaces the constructor for setting initial state, facilitating upgrades through new logic contracts.
  • Proxy Pattern: Utilizes a proxy contract to delegate calls to this logic contract, allowing for future upgrades without losing the contract's state.

This restructuring prepares the CrowdfundingCampaign contract for upgradeability.


Compile the updated CrowdfundingCampaign contract

Now that the CrowdfundingCampaign contract is adapted for contract upgradability, let's proceed to deploy the contract so we may upgrade it in later steps. Since we've made changes to our contract we will need to re-compile.

To compile the contracts in the project, run the following command:

npm run compile

Upon successful compilation, you'll receive output detailing the zksolc and solc versions used during compiling and the number of Solidity files compiled.

Compiling contracts for ZKsync Era with zksolc v1.4.0 and solc v0.8.17
Compiling 29 Solidity file
Successfully compiled 29 Solidity file

The compiled artifacts will be located in the /artifacts-zk folder.

Deploy the beacon and contract

You'll find the necessary deployment script at /deploy/deployBeaconProxy.ts.

deployBeaconProxy.ts
import { getWallet } from "./utils";
import { Deployer } from '@matterlabs/hardhat-zksync';
import { ethers } from "ethers";
import { HardhatRuntimeEnvironment } from "hardhat/types";

export default async function (hre: HardhatRuntimeEnvironment) {
    const wallet = getWallet();
    const deployer = new Deployer(hre, wallet);

    const contractArtifact = await deployer.loadArtifact("CrowdfundingCampaign");
    const fundingGoalInWei = ethers.parseEther('0.1').toString();

    const beacon = await hre.zkUpgrades.deployBeacon(
        getWallet(),
        contractArtifact
    );
    await beacon.waitForDeployment();

    const crowdfunding = await hre.zkUpgrades.deployBeaconProxy(deployer.zkWallet,
        await beacon.getAddress(), contractArtifact, [fundingGoalInWei]);

    await crowdfunding.waitForDeployment();
}

Key Components:

  • deployBeacon Method: Initiates the deployment of a beacon contract, which acts as a central point for managing future upgrades of the CrowdfundingCampaign contract. The beacon's address is a critical component as it links the deployed proxy to the actual contract logic.
  • deployBeaconProxy Method: This step involves deploying the beacon proxy, which serves as the user-facing contract instance. It references the beacon for its logic, allowing for seamless upgrades without altering the proxy's address. The fundingGoalInWei parameter, converted from ether to wei, is passed during this step to initialize the contract with a funding goal.

Execute the deployment command corresponding to your package manager. The default command deploys to the configured network in your Hardhat setup. For local deployment, append --network inMemoryNode to deploy to the local in-memory node running.

npx hardhat deploy-zksync --script deployBeaconProxy.ts
# To deploy the contract on local in-memory node:
# npx hardhat deploy-zksync --script deployBeaconProxy.ts --network inMemoryNode

Upon successful deployment, you'll receive output detailing the deployment process, including the contract addresses of the implementation contract, the admin contract, and the beacon proxy contract.

Beacon impl deployed at 0xE3F814fa915A75bA47230537726C99f6517Da58e
Beacon deployed at:  0x26410Bebf5Df7398DCBC5f00e9EBBa0Ddf471C72
Beacon proxy deployed at:  0xD58FA9Fb362Abf69cFc68A3545fD227165DAc167

Compile the CrowdfundingCampaignV2 Contract

With our initial setup deployed, we're ready to upgrade our CrowdfundingCampaign.sol contract by incorporating a deadline for contributions. This addition not only brings a new layer of functionality but also introduces the concept of time-based conditions through a modifier.

Current Contract Overview:

The existing version of our contract allows for open-ended contributions towards a funding goal, without any time constraints.

Proposed Upgrade:

We're introducing a deadline variable, initialized at contract deployment, to establish a clear timeframe for accepting contributions. The withinDeadline modifier will then enforce this constraint, ensuring contributions are made within the allowed period.

Enhanced Contract:

The upgraded contract, CrowdfundingCampaignV2.sol, located in the /contracts directory, incorporates these changes:

  • Deadline Variable: A new state variable deadline defines the campaign's end time, enhancing the contract with time-based logic.
  • Initialization Logic: An additional initialization method, initializeV2, sets the deadline based on a duration provided during the upgrade. This function ensures that the upgrade is backward-compatible and maintains the contract's integrity.
  • Contribution Logic with Deadline: The contribute method now includes a withinDeadline modifier, ensuring all contributions are made within the set timeframe.
  • Deadline Enforcement: The withinDeadline modifier checks the current time against the deadline, safeguarding the contract from late contributions.

Deadline Extension Capability:

To provide flexibility, a new function allows the owner to extend the deadline, offering adaptability to changing campaign needs.

CrowdfundingCampaignV2.sol
function extendDeadline(uint256 _newDuration) public {
    require(msg.sender == owner, "Only the owner can extend the deadline");
    deadline = block.timestamp + _newDuration;
}

This upgrade not only introduces the element of time to the campaign but also exemplifies the use of modifiers for enforcing contract conditions.

Smart contracts deployed to ZKsync must be compiled using our custom compiler. zksolc is the compiler used for Solidity.

To compile the contracts in a project, run the following command:

npm run compile

Upon successful compilation, you'll receive output detailing the zksolc and solc versions used during compiling and the number of Solidity files compiled.

Compiling contracts for ZKsync Era with zksolc v1.4.0 and solc v0.8.17
Compiling 4 Solidity file
Successfully compiled 4 Solidity file

The compiled artifacts will be located in the /artifacts-zk folder.

Upgrade to CrowdfundingCampaignV2

This section describes the upgrade process to CrowdfundingCampaignV2.sol contract. Let's start by reviewing the upgradeBeaconCrowdfundingCampaign.ts script in the deploy/upgrade-scripts directory:

Make sure to replace YOUR_BEACON_ADDRESS_HERE with the address of your deployed beacon and YOUR_PROXY_ADDRESS_HERE with the actual address of your deployed Beacon Proxy from the previous deployment step.

upgradeBeaconCrowdfundingCampaign.ts
import { getWallet } from "../utils";
import { Deployer } from '@matterlabs/hardhat-zksync';
import { HardhatRuntimeEnvironment } from "hardhat/types";
import * as zk from 'zksync-ethers';
import { Contract } from 'ethers';

export default async function (hre: HardhatRuntimeEnvironment) {
    const wallet = getWallet();
    const deployer = new Deployer(hre, wallet);

    // Placeholder for the deployed beacon address
    const beaconAddress = 'YOUR_BEACON_ADDRESS_HERE';

    const contractV2Artifact = await deployer.loadArtifact('CrowdfundingCampaignV2');

    // Upgrade the proxy to V2
    await hre.zkUpgrades.upgradeBeacon(deployer.zkWallet, beaconAddress, contractV2Artifact);

    console.log('Successfully upgraded crowdfundingCampaign to crowdfundingCampaignV2');

    const attachTo = new zk.ContractFactory<any[], Contract>(
        crowdfundingCampaignV2.abi,
        crowdfundingCampaignV2.bytecode,
        deployer.zkWallet,
        deployer.deploymentType,
    );

    // Placeholder for the deployed beacon proxy address
    const proxyAddress = 'YOUR_PROXY_ADDRESS_HERE';

    const upgradedContract  = attachTo.attach(proxyAddress);

    upgradedContract.connect(deployer.zkWallet);
    // wait some time before the next call
    await new Promise((resolve) => setTimeout(resolve, 2000));

    // Initialize V2 with a new campaign duration
    const durationInSeconds = 30 * 24 * 60 * 60; // For example, setting a 30-day duration
    const initTx = await upgradedContract.initializeV2(durationInSeconds);
    const receipt = await initTx.wait();

   console.log(`CrowdfundingCampaignV2 initialized. Transaction Hash: ${receipt.hash}`);
}

Key Components:

  • upgradeBeacon: This method from the hre.zkUpgrades module is used to update the beacon contract with the new version of the contract logic, CrowdfundingCampaignV2. It ensures that all proxies pointing to this beacon will now execute the updated contract code.
  • initializeV2: This method is specifically called post-upgrade to initialize or reconfigure any new state variables or logic introduced in the CrowdfundingCampaignV2. Here, it's used to set a new campaign duration, seamlessly integrating new functionalities while retaining the existing contract state and funds.

Execute the test command corresponding to your package manager:

npx hardhat deploy-zksync --script upgrade-scripts/upgradeBeaconCrowdfundingCampaign.ts

Upon successful deployment, you'll receive output detailing the upgrade process, including the new beacon address, and transaction hash:

New beacon impl deployed at 0x58BD5adb462CF087E5838d53aE38A3Fe0EAf7A31
Successfully upgraded crowdfundingCampaign to crowdfundingCampaignV2 0x26410Bebf5Df7398DCBC5f00e9EBBa0Ddf471C72
CrowdfundingCampaignV2 initialized! 0x5f3131c77fcac19390f5f644a3ad1f0e7719dee4b4b5b4746c992de00db743f7
Fundraising goal: 100000000000000000

Verify upgradable contracts

For the verification of our upgradable contracts, it's essential to utilize the proxy address that was specified in our upgrade script.

To proceed with verification, execute the following command:

npx hardhat verify <BEACON-PROXY-ADDRESS>

Upon successful verification, you'll receive output detailing the verification process:

Verifying implementation: 0x58BD5adb462CF087E5838d53aE38A3Fe0EAf7A31
Your verification ID is: 10547
Contract successfully verified on ZKsync block explorer!
Verifying beacon: 0x26410Bebf5Df7398DCBC5f00e9EBBa0Ddf471C72
Your verification ID is: 10548
Contract successfully verified on ZKsync block explorer!
Verifying beacon proxy: 0xD58FA9Fb362Abf69cFc68A3545fD227165DAc167
Your verification ID is: 10549
Contract successfully verified on ZKsync block explorer!

🎉 Congratulations! The CrowdfundingCampaignV2 contract has been upgraded and verified!

Coming soon!

What is a UUPS upgradeable contract?

UUPS (Universal Upgradeable Proxy Standard) Upgradeable Contracts embed the upgrade logic within the contract itself, simplifying upgrades and enhancing security. The components are:

  1. Proxy Contract: Contains minimal logic, primarily delegating calls to the implementation contract. Unlike other proxies, it doesn't require a separate upgrade function.
  2. Implementation Contract: Houses the business logic and the upgrade functionality, enabling the contract to upgrade itself from within.
  3. Admin Role: Assigned to an entity with the authority to initiate upgrades, ensuring controlled access to the upgrade function.

In UUPS contracts, upgrades are performed by invoking the upgrade function within the implementation contract, which updates the proxy's reference to point to a new implementation. This self-contained approach minimizes the proxy's complexity and gas costs, while the implementation contract's built-in upgrade mechanism ensures only authorized upgrades. The contract's state remains intact across upgrades, facilitating continuous improvement with a stable user experience.


Run the following command in your terminal to initialize the project.

npx zksync-cli@latest create --template qs-upgrade contract-upgrade-quickstart
cd contract-upgrade-quickstart
If you encounter an error while installing project dependencies using NPM as your package manager, try running npm install --force.

Set up your wallet

Deploying contracts on the ZKsync Sepolia Testnet requires having testnet ETH. If you're working within the local development environment, you can utilize pre-configured rich wallets and skip this step. For testnet deployments, you should have your wallet funded from the previous step.


Adapt the CrowdfundingCampaign.sol for UUPS Upgradability

To align the CrowdfundingCampaign.sol contract with UUPS (Universal Upgradeable Proxy Standard) upgradability, we're integrating OpenZeppelin's UUPSUpgradeable contracts. This method offers a more secure and gas-efficient approach to contract upgrades by embedding the upgrade logic within the contract itself.

Refactoring for UUPS Compatibility

We've refactored the contract to support UUPS upgradability, ensuring the contract's logic is upgradeable while maintaining a persistent state. This is achieved by utilizing initializer functions and the UUPS upgrade mechanism.

UUPS-Enabled Contract Structure:

CrowdfundingCampaign_UUPS.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Import UUPS from OpenZeppelin
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract CrowdfundingCampaign_UUPS is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    uint256 public fundingGoal;
    uint256 public totalFundsRaised;
    mapping(address => uint256) public contributions;

    event ContributionReceived(address contributor, uint256 amount);
    event GoalReached(uint256 totalFundsRaised);

    // Initializer function, replaces constructor for upgradeable contracts
    function initialize(uint256 _fundingGoal) public initializer {
        __Ownable_init(); // Initialize ownership to the deployer
        __UUPSUpgradeable_init(); // Initialize UUPS upgradeability

        fundingGoal = _fundingGoal;
    }

    function contribute() public payable {
        // Contribution logic remains the same
    }

    function withdrawFunds() public onlyOwner {
        // WithdrawFunds logic remains the same
    }

    function getTotalFundsRaised() public view returns (uint256) {
        // getTotalFundsRaised remains the same
    }

    function getFundingGoal() public view returns (uint256) {
        // getFundingGoal remains the same
    }

    // Ensure only the owner can upgrade the contract
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

Key Adaptations:

  • Initializable & UUPSUpgradeable: The contract inherits from Initializable and UUPSUpgradeable, ensuring initialization follows the proxy pattern and enabling the UUPS upgrade mechanism.
  • OwnableUpgradeable: Utilizes OwnableUpgradeable to manage ownership through an initializer, important for secure upgrade authorization.
  • _authorizeUpgrade: A safeguard function ensuring only the contract owner can perform upgrades, reinforcing the contract's security.

By adopting the UUPS pattern, the CrowdfundingCampaign_UUPS contract becomes efficiently upgradeable, offering enhanced security and reduced gas costs, setting a solid foundation for future enhancements.


Compile the CrowdfundingCampaign_UUPS contract

Now that the CrowdfundingCampaign_UUPS contract is adapted for contract upgradability, let's proceed to deploy the contract so we may upgrade it in later steps. Since we've made changes to our contract we will need to re-compile.

To compile the contracts in the project, run the following command:

npm run compile

Upon successful compilation, you'll receive output detailing the zksolc and solc versions used during compiling and the number of Solidity files compiled.

Compiling contracts for ZKsync Era with zksolc v1.4.0 and solc v0.8.17
Compiling 4 Solidity file
Successfully compiled 4 Solidity file

The compiled artifacts will be located in the /artifacts-zk folder.

Deploy the updated contract

The script to deploy the CrowdfundingCampaign_UUPS contract is located at /deploy/deployUUPS.ts.

deployUUPS.ts
import { getWallet } from "./utils";
import { Deployer } from '@matterlabs/hardhat-zksync';
import { ethers } from "ethers";
import { HardhatRuntimeEnvironment } from "hardhat/types";

export default async function (hre: HardhatRuntimeEnvironment) {
    const wallet = getWallet();
    const deployer = new Deployer(hre, wallet);

    const contractArtifact = await deployer.loadArtifact("CrowdfundingCampaign_UUPS");
    const fundingGoalInWei = ethers.parseEther('0.1').toString();

    const crowdfunding = await hre.zkUpgrades.deployProxy(
        getWallet(),
        contractArtifact,
        [fundingGoalInWei],
        { initializer: 'initialize' }
    );

    await crowdfunding.waitForDeployment();
}

Key Components:

  • deployProxy Method: This method is responsible for deploying the CrowdfundingCampaign contract as a UUPS upgradeable contract. It initializes the contract with the specified parameters, such as the fundingGoalInWei, ensuring that the contract is ready for immediate use after deployment. The use of the UUPS pattern provides a secure and efficient mechanism for future upgrades.
  • initializer Option: Specifies the initialization method of the contract, in this case, initialize. This is used for setting up the initial state of the contract upon deployment, particularly important for upgradeable contracts where constructor usage is not possible.

Execute the deployment command corresponding to your package manager. The default command deploys to the configured network in your Hardhat setup. For local deployment, append --network inMemoryNode to deploy to the local in-memory node running.

npx hardhat deploy-zksync --script deployUUPS.ts
# To deploy the contract on local in-memory node:
# npx hardhat deploy-zksync --script deployUUPS.ts --network inMemoryNode

Upon successful deployment, you'll receive output detailing the deployment process, including the contract addresses of the implementation contract, the admin contract, and the transparent proxy contract.

Implementation contract was deployed to 0xF0De77041F3cF6D9C905A10ce59858b17E57E3B9
UUPS proxy was deployed to 0x56882194aAe8E4B6d18cD84e4D7B0F807e0100Cb

Upgrade to the CrowdfundingCampaignV2_UUPS Contract

With our initial setup deployed, we're ready to upgrade our CrowdfundingCampaign_UUPS.sol contract by incorporating a deadline for contributions. This addition not only brings a new layer of functionality but also introduces the concept of time-based conditions through a modifier.

Current Contract Overview:

The existing version of our contract allows for open-ended contributions towards a funding goal, without any time constraints.

Proposed Upgrade:

We're introducing a deadline variable, initialized at contract deployment, to establish a clear timeframe for accepting contributions. The withinDeadline modifier will then enforce this constraint, ensuring contributions are made within the allowed period.

Enhanced Contract:

The upgraded contract, CrowdfundingCampaignV2_UUPS.sol, located in the /contracts directory, incorporates these changes:

  • Deadline Variable: A new state variable deadline defines the campaign's end time, enhancing the contract with time-based logic.
  • Initialization Logic: An additional initialization method, initializeV2, sets the deadline based on a duration provided during the upgrade. This function ensures that the upgrade is backward-compatible and maintains the contract's integrity.
  • Contribution Logic with Deadline: The contribute method now includes a withinDeadline modifier, ensuring all contributions are made within the set timeframe.
  • Deadline Enforcement: The withinDeadline modifier checks the current time against the deadline, safeguarding the contract from late contributions.

Deadline Extension Capability:

To provide flexibility, a new function allows the owner to extend the deadline, offering adaptability to changing campaign needs.

CrowdfundingCampaignV2_UUPS.sol
function extendDeadline(uint256 _newDuration) public {
    require(msg.sender == owner, "Only the owner can extend the deadline");
    deadline = block.timestamp + _newDuration;
}

This upgrade not only introduces the element of time to the campaign but also exemplifies the use of modifiers for enforcing contract conditions.

Compile the CrowdfundingCampaignV2_UUPS contract

Smart contracts deployed to ZKsync must be compiled using our custom compiler. zksolc is the compiler used for Solidity.

To compile the contracts in a project, run the following command:

npm run compile

Upon successful compilation, you'll receive output detailing the zksolc and solc versions used during compiling and the number of Solidity files compiled.

Compiling contracts for ZKsync Era with zksolc v1.4.0 and solc v0.8.17
Compiling 4 Solidity file
Successfully compiled 4 Solidity file

The compiled artifacts will be located in the /artifacts-zk folder.

Upgrade to CrowdfundingCampaignV2_UUPS

This section describes the initiating the upgrade to CrowdfundingCampaignV2_UUPS.sol contract. Let's start by reviewing the upgradeUUPSCrowdfundingCampaign.ts script in the deploy/upgrade-scripts directory:

Replace YOUR_PROXY_ADDRESS_HERE with the actual address of your deployed Transparent Proxy from the previous deployment step.

upgradeUUPSCrowdfundingCampaign.ts
import { getWallet } from "../utils";
import { Deployer } from '@matterlabs/hardhat-zksync';
import { HardhatRuntimeEnvironment } from "hardhat/types";

export default async function (hre: HardhatRuntimeEnvironment) {
    const wallet = getWallet();
    const deployer = new Deployer(hre, wallet);

    // Placeholder for the deployed proxy address
    const proxyAddress = 'YOUR_PROXY_ADDRESS_HERE';

    // Upgrade the proxy to V2
    const contractV2Artifact = await deployer.loadArtifact('CrowdfundingCampaignV2_UUPS');
    const upgradedContract = await hre.zkUpgrades.upgradeProxy(deployer.zkWallet, proxyAddress, contractV2Artifact);
    console.log('Successfully upgraded crowdfundingCampaign_UUPS to crowdfundingCampaignV2_UUPS');

    upgradedContract.connect(deployer.zkWallet);
    // wait some time before the next call
    await new Promise((resolve) => setTimeout(resolve, 2000));

    const durationInSeconds = 30 * 24 * 60 * 60; // For example, setting a 30-day duration

    const initTx = await upgradedContract.initializeV2(durationInSeconds);
    const receipt = await initTx.wait();

    console.log('CrowdfundingCampaignV2_UUPS initialized!', receipt.hash);
}

Key Components:

  • upgradeProxy: A critical method from the hre.zkUpgrades module that performs the contract upgrade. It takes the wallet, the proxy address, and the new contract artifact as arguments to transition the proxy to use the CrowdfundingCampaignV2_UUPS logic.
  • initializeV2: Post-upgrade, this function is invoked to initialize the new variables or logic introduced in CrowdfundingCampaignV2_UUPS. In this example, it sets a new campaign duration, illustrating how contract upgrades can add functionalities without losing the existing state or funds.

Execute the test command corresponding to your package manager:

npx hardhat deploy-zksync --script upgrade-scripts/upgradeUUPSCrowdfundingCampaign.ts

Upon successful deployment, you'll receive output detailing the upgrade process, including the new beacon address, and transaction hash:

Contract successfully upgraded to 0x9BE22706966D717d7b0C8aEC99A1a9d1b3bFeC50 with tx 0x24ad582828b23b98d207ec7c057cd6a9c911bea22dbe85e0affd7479b00d90e9
Successfully upgraded crowdfundingCampaign_UUPS to crowdfundingCampaignV2_UUPS
CrowdfundingCampaignV2_UUPS initialized! 0xab959f588b64dc6dee1e94d5fa0da2ae205c7438cf097d26d3ba73690e2b09e8

Verify upgradable contracts

To verify our upgradable contracts we need to the proxy address we previously used in our upgrade script. With that execute the following command:

npx hardhat verify <PROXY-ADDRESS>

Upon successful verification, you'll receive output detailing the verification process:

Verifying implementation: 0x9BE22706966D717d7b0C8aEC99A1a9d1b3bFeC50
Your verification ID is: 10618
Contract successfully verified on ZKsync block explorer!
Verifying proxy: 0x91921fDb0F8942c18eCeE4E3896b369ca0650483
Your verification ID is: 10619
Contract successfully verified on ZKsync block explorer!

🎉 Congratulations! The CrowdfundingCampaignV2_UUPS contract has been upgraded and verified!

Coming soon!

Takeaways

  • Upgradability: The guide highlights the critical aspect of smart contract upgradability, introducing techniques for using transparent, beacon, and UUPS proxies. This ensures your contracts remain adaptable, allowing for seamless updates to business logic or enhancements in efficiency.
  • Flexibility: Emphasizing flexibility, the guide demonstrates how upgradable contracts maintain continuity of state and fund security, even as underlying functionalities evolve. This approach provides a resilient framework for your dApps to grow and adapt over time.

Next Steps

  • Exploring Paymasters: Continue on to the next guide focused on using paymasters with your smart contracts. Paymasters abstract gas payments in transactions, offering new models for transaction fee management and enhancing user experience in dApps.
  • Advanced ZKsync Integrations: Explore deeper into ZKsync's ecosystem by implementing features like account abstraction and paymasters to enhance user experience and contract flexibility.
  • Community Engagement and Contribution: Join the vibrant ZKsync community. Participate in forums, Discord, or GitHub discussions. Sharing insights, asking queries, and contributing can enrich the ecosystem and your understanding of ZKsync.

Made with ❤️ by the ZKsync Community