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 upgradeability, 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 upgradeability 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.

Setup the project

Make sure to go through the setup provided in the initial Getting started section. You will have downloaded the 101 project through ZKsync CLI create and started up a local in-memory node for development.

If you haven't started up your local in-memory node or you're not sure, run the following:

zksync-cli dev restart

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.


Adapt CrowdfundingCampaign.sol contract for upgradeability

To adapt our CrowdfundingCampaign.sol contract for upgradeability, 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/3-proxy-contracts/transparent directory you'll observe the refactored ProxyableCrowdfundingCampaign.sol contract which initializes state variables through an initialize function instead of the constructor, in line with the Transparent Proxy pattern.

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 makes the ProxyableCrowdfundingCampaign contract upgradeable.


Deploy the ProxyableCrowdfundingCampaign contract

Now that the ProxyableCrowdfundingCampaign contract is adapted for contract upgradeability, let's proceed to deploy the contract so we may upgrade it in later steps.

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

npm
npm run compile

The deployment script is located at /deploy/3-proxy-contracts/transparent/deploy.ts.

Key Components:

  • hre.zkUpgrades.deployProxy: This method call deploys the ProxyableCrowdfundingCampaign 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.
npm
npm run deploy:transparent-proxy

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 ProxyableCrowdfundingCampaign Contract

With our initial setup deployed, we're ready to update our ProxyableCrowdfundingCampaign.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 dealine on creation of a crowdfunding campaign. Contributions can only be made within the allowed time period.

Enhanced Contract:

The upgraded contract, /contracts/3-proxy-contracts/transparent/V2_ProxyableCrowdfundingCampaign.sol, 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. Campaigns that were made before the upgrade can still continue to fund without the deadline logic affecting them.
  • 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 that the campaign is a V2 version and 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.

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

Compile contract

Run the npm script compile to compile the contracts:

npm
npm run compile

Update to V2_ProxyableCrowdfundingCampaign

This section guides you through upgrading the ProxyableCrowdfundingCampaign contract to its second version, V2_ProxyableCrowdfundingCampaign. Review the deploy/3-proxy-contracts/upgrade-transparent.ts script to begin.

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

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 V2CrowdfundingCampaign logic.
  • initializeV2: Post-upgrade, this function is invoked to initialize the new variables or logic introduced in V2_ProxyableCrowdfundingCampaign. In this example, it sets a new campaign duration, illustrating how contract upgrades can add functionalities without losing the existing state or funds.

Run the following command to upgrade to the V2_ProxyableCrowdfundingCampaign:

npm
npm run upgrade:transparent-proxy

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

Contract successfully upgraded to 0x094499Df5ee555fFc33aF07862e43c90E6FEe501 with tx 0xe281c711b08cab3177b3a542af2e7e3def6602e8d34284127a4343b8e95dcf82
Successfully upgraded ProxyableCrowdfundingCampaign to V2_ProxyableCrowdfundingCampaign
V2CrowdfundingCampaign initialized. Transaction Hash: 0x3a7cbf9d584457bc6b452964f41e1971f22393724f103e41984e0282bd8cb5cc

Verify upgradable contracts

Since we are using in memory node for our smart contracts, we do not have the feature available to verify the smart contract.The following explains how you can verify an upgraded smart contract on testnet or mainnet.

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. This is the address from the earlier deployment message: Contract successfully upgraded to <PROXY_ADDRESS>.

npm
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!

What is a beacon proxy upgradeable contract?

Beacon Proxy Upgradeable Contracts allows 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.


Adapt the Crowdfunding Campaign contract for upgradeability

To adapt our Crowdfunding Campaign contract for upgradeability, 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/3-proxy-contracts/beacon directory you'll observe the refactored BeaconCrowdfundingCampaign contract which initializes state variables through an initialize function instead of the constructor, in line with the proxy pattern.

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 BeaconCrowdfundingCampaign contract for upgradeability.


Compile the BeaconCrowdfundingCampaign contract

Now that the BeaconCrowdfundingCampaign contract is adapted for contract upgradeability, let's proceed to deploy the contract so we may upgrade it in later steps.

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

npm
npm run compile

Deploy the beacon and contract

You'll find the necessary deployment script at deploy/3-proxy-contracts/beacon/deploy.ts.

Key Components:

  • deployBeacon Method: Initiates the deployment of a beacon contract, which acts as a central point for managing future upgrades of the BeaconCrowdfundingCampaign 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.

Run the following command to deploy our contract with a beacon proxy:

npm
npm run deploy:beacon-proxy

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 V2_BeaconCrowdfundingCampaign Contract

With our initial setup deployed, we're ready to upgrade our BeaconCrowdfundingCampaign.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, V2_BeaconCrowdfundingCampaign.sol, located in the /contracts/3-proxy-contracts/beacon 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 in the contribute method, safeguarding the contract from late contributions.

Deadline Extension Capability:

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

Compile the V2_UUPSCrowdfundingCampaign contract

Run the npm script compile to compile the contracts:

npm
npm run compile

Deploy the upgrade to V2_BeaconCrowdfundingCampaign

This section describes the upgrade process to the V2_BeaconCrowdfundingCampaign.sol contract. Let's start by reviewing the deploy/3-proxy-contracts/beacon/upgrade.ts script.

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.

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, V2_BeaconCrowdfundingCampaign. 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 V2_BeaconCrowdfundingCampaign. Here, it's used to set a new campaign duration, seamlessly integrating new functionalities while retaining the existing contract state and funds.

Run the upgrade npm script:

npm
npm run upgrade:beacon-proxy

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 BeaconCrowdfundingCampaign to V2_BeaconCrowdfundingCampaign
V2_BeaconCrowdfundingCampaign initialized. Transaction Hash: 0x5f3131c77fcac19390f5f644a3ad1f0e7719dee4b4b5b4746c992de00db743f7

Verify upgradeable contracts

Since we are using the in memory node for our smart contracts, we do not have the feature available to verify the smart contract.The following explains how you can verify an upgraded smart contract on testnet or mainnet.

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

To proceed with verification, execute the following command:

npm
npx hardhat verify <YOUR_BEACON_PROXY_HERE>

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!

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.


Adapt the crowdfunding campaign code for UUPS Upgradability

To align the Crowdfunding Campaign contract with UUPS (Universal Upgradeable Proxy Standard) upgradeability, 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 upgradeability, 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:

In the contracts/3-proxy-contracts/uups directory you'll find the refactored UUPSCrowdfundingCampaign.sol contract.

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.

Compile the UUPSCrowdfundingCampaign contract

Now that the UUPSCrowdfundingCampaign contract is adapted for contract upgradeability, let's proceed to deploy the contract so we may upgrade it in later steps.

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

npm
npm run compile

Deploy the updated contract

The script to deploy the UUPSCrowdfundingCampaign contract is located at /deploy/3-proxy-contracts/uups/deployUUPS.ts.

Key Components:

  • deployProxy Method: This method is responsible for deploying the UUPSCrowdfundingCampaign 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.
npm
npm run deploy:uups-proxy

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 V2_UUPSCrowdfundingCampaign Contract

With our initial setup deployed, we're ready to upgrade our UUPSCrowdfundingCampaign.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, V2_UUPSCrowdfundingCampaign.sol, located in the /contracts/3-proxy-contracts/uups 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 that the campaign is a V2 version and checks the current time against the deadline, safeguarding the contract from late contributions.

Deadline Extension Capability:

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

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

Compile the V2_UUPSCrowdfundingCampaign contract

Run the npm script compile to compile the contracts:

npm
npm run compile

Deploy the upgrade to V2_UUPSCrowdfundingCampaign

This section describes upgrading from the original crowdfunding campaign contract to the updated V2_UUPSCrowdfundingCampaign.sol contract. Let's start by reviewing the deploy/3-proxy-contracts/uups/upgrade.ts script.

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

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 V2_UUPSCrowdfundingCampaign logic.
  • initializeV2: Post-upgrade, this function is invoked to initialize the new variables or logic introduced in V2_UUPSCrowdfundingCampaign. 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 upgrade npm script command to upgrade:

npm
npm run upgrade:uups-proxy

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

Contract successfully upgraded to 0x9BE22706966D717d7b0C8aEC99A1a9d1b3bFeC50 with tx 0x24ad582828b23b98d207ec7c057cd6a9c911bea22dbe85e0affd7479b00d90e9
Successfully upgraded UUPSCrowdfundingCampaign to V2_UUPSCrowdfundingCampaign
V2_UUPSCrowdfundingCampaign initialized! Transaction Hash: 0xab959f588b64dc6dee1e94d5fa0da2ae205c7438cf097d26d3ba73690e2b09e8

Verify upgradable contracts

Since we are using the in memory node for our smart contracts, we do not have the feature available to verify the smart contract.The following explains how you can verify an upgraded smart contract on testnet or mainnet.

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

npm
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!

Takeaways

  • Upgradeability: The guide highlights the critical aspect of smart contract upgradeability, 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 developer 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