This is a premium alert message you can set from Layout! Get Now!

Using the UUPS proxy pattern to upgrade smart contracts

0

We all know that one of the most impressive features of the blockchain is its immutability property. But it is not advantageous in all cases.

Imagine a deployed smart contract that holds user funds having a vulnerability. Developers should fix the bug as early as possible, like what they can do in the Web2 applications. But in terms of Web3 development, it’s not the same case.

Traditional smart contract patterns don’t allow such hot fixes. Instead, the developers need to deploy a new contract every time they want to add a feature or fix a bug. While it doesn’t seem like a big issue in the beginning, it’ll be a huge overhead when the codebase grows. And every time, the data needs to be migrated from the old contract to a new contract to reflect the current state of the protocol.

To solve this problem, various upgradability patterns have been introduced. Among them, the proxy pattern is considered the truest form of upgradability.

When we speak about upgradability, it means that the client always interacts with the same contract (proxy), but the underlying logic can be changed (upgraded) whenever needed without losing any previous data.

Web Chart Denoting Storage Structure

N.B., one can argue that via upgradable proxies, a protocol can even change the underlying logic for their needs (even without the knowledge of their community). There are various methods to prevent that, as DAOs follow timelocks. But, it’s beyond the scope of the article and that itself is a topic for another day.

Types of proxy patterns

Currently, there are three types of proxy patterns:

OpenZeppelin suggests using the UUPS pattern as it is more gas efficient.

Enough of the introduction and theory. Let’s deploy an upgradable Pizza contract using the UUPS proxy pattern, leveraging Hardhat and OpenZeppelin’s UUPS library contracts.

Here’s what we’ll cover:

It’s demo time: Setup with Hardhat and OpenZeppelin

We’ll deploy a simple smart contract called Pizza and upgrade it to PizzaV2 using the UUPS proxy pattern.

As we’ll be using Hardhat for development purposes, you’ll need to have NodeJS and NPM installed in your machine.

Once Node.js is installed and set up, you can install Hardhat globally in your machine from your command line by running the command npm install hardhat -g.

Once Hardhat is installed, you can create new Hardhat projects easily!

Let’s create a fresh new directory for our project:

mkdir uups-demo && cd uups-demo

Initialize a new Hardhat project by running npx hardhat and choosing the initial config for the project.

Initialize A New Hardhat Project

Now Hardhat will install some of the required libraries. Other than that, we’ll require some additional npm modules as well for the UUPS pattern. Run the following command to install the modules.

npm i @openzeppelin/contracts-upgradeable @openzeppelin/hardhat-upgrades @nomiclabs/hardhat-etherscan dotenv --save-dev

Once everything has been installed, the initial directory structure will look something like this:

Initial Directory Structure

N.B., the file names and the contents will be modified as we proceed.

Making implementation and proxy contracts

Create a new file called Pizza.sol inside the contracts directory and add the following code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

// Open Zeppelin libraries for controlling upgradability and access.
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract Pizza is Initializable, UUPSUpgradeable, OwnableUpgradeable {
   uint256 public slices;

      ///@dev no constructor in upgradable contracts. Instead we have initializers
    ///@param _sliceCount initial number of slices for the pizza
   function initialize(uint256 _sliceCount) public initializer {
       slices = _sliceCount;

      ///@dev as there is no constructor, we need to initialise the OwnableUpgradeable explicitly
       __Ownable_init();
   }

   ///@dev required by the OZ UUPS module
   function _authorizeUpgrade(address) internal override onlyOwner {}

   ///@dev decrements the slices when called
   function eatSlice() external {
       require(slices > 1, "no slices left");
       slices -= 1;
   }
}

This is a simple Pizza contract that has three methods:

  • initialize() — Upgradable contracts should have an initialize method in place of constructors, and also the initializer keyword makes sure that the contract is initialized only once
    • _authorizeUpgrade() — This method is required to safeguard from unauthorized upgrades because in the UUPS pattern the upgrade is done from the implementation contract, whereas in the transparent proxy pattern, the upgrade is done via the proxy contract
  • _eatSlice() — A simple function to reduce the slice count whenever called

Now let’s compile and deploy the Pizza contract.

Before doing that, we have to update the hardhat.config.js file with the following contents:

require("@nomiclabs/hardhat-ethers");
require("@openzeppelin/hardhat-upgrades");
require("@nomiclabs/hardhat-etherscan");

require("dotenv").config();

module.exports = {
 solidity: "0.8.10",
 networks: {
   kovan: {
     url: `https://kovan.infura.io/v3/${process.env.INFURA_API_KEY}`,
     accounts: [process.env.PRIVATE_KEY],
   },
 },
 etherscan: {
   apiKey: process.env.ETHERSCAN_API_KEY,
 },
};

Create a new file called .env and add the following contents:

PRIVATE_KEY = <<DEPLOYER_PRIVATE_KEY>>
ETHERSCAN_API_KEY = <<ETHERSCAN_API_KEY>>
INFURA_API_KEY=  <<INFURA_API_KEY>>

The PRIVATE_KEY is the private key of the deployer wallet. You can grab the INFURA_API_KEY from here and ETHERSCAN_API_KEY from here.

Once the .env file is created, you can compile the contracts by running npx hardhat compile in your terminal.

Now let’s deploy our contract. Inside the scripts directory, create a new file called deploy_pizza_v1.js and add the following contents:

const { ethers, upgrades } = require("hardhat");

const SLICES = 8;
async function main() {
 const Pizza = await ethers.getContractFactory("Pizza");

 console.log("Deploying Pizza...");

 const pizza = await upgrades.deployProxy(Pizza, [SLICES], {
   initializer: "initialize",
 });
 await pizza.deployed();

 console.log("Pizza deployed to:", pizza.address);
}

main();

Save the file.

Now you can deploy the contracts by running the following command in the terminal:

npx hardhat run ./scripts/deploy_pizza_v1.js –network kovan

You should see something like this. The address will be different!

Deploying The Contracts

The address displayed in the console is the address of the proxy contract. If you visit Etherscan and search the deployer address, you’ll see two new contracts created via two transactions. The first one is the actual Pizza contract (the implementation contract), and the second one is the proxy contract.

In my case, the Pizza contract address is 0x79928a69ada394ad454680d3c4bd2197ad9f7a94.

The proxy contract address is 0x9bBADFcDF4589C6a6179Ee48b7fa7eeeCf4d801c.

You can copy the address of the Pizza contract from Etherscan and verify it by running the command below:

npx hardhat verify –network kovan <<CONTRACT_ADDRESS>>

The output should be something like this:

The Output Of The Pizza Contract From Etherscan

N.B., after deployment, if you’re confused which contract is the proxy or implementation, the proxy contract source code will be already verified on Etherscan (in most cases). The unverified will be the implementation contract!

Once verified, your Etherscan transactions will look like this:

Verified Etherscan Transactions

If you check the Pizza contract in Etherscan, the values like owner, slices, etc. will not be set or initialized because in the proxy pattern, everything is stored and executed in the context of the proxy contract.

So in order to interact with the Pizza contract, you should do it via the proxy contract. To do that, first we need to inform Etherscan that the deployed contract is actually a proxy.

In the Contract tab of the proxy contract, there’ll be a small dropdown above the source code section (on the right side).

Inform Etherscan That Deployed Contract Is A Proxy

Choose “Is this a proxy?” option from the dropdown and then Verify.

Proxy Contract Verification Page

You can see Read as Proxy and Write as Proxy options in the Contract tab of the proxy contract.

Contract Tab Of The Proxy Contract

Now you can interact with the Pizza contract using those options!

Upgrading the contract

After some time passes, let’s say we have to include additional functionality to our Pizza contract. For example, let’s make a simple function to refill slices and a function to return the current contract version.

We can create our PizzaV2 contract. Inside the contracts folder, create a new file called PizzaV2.sol, add the following contents, and save the file:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import "./Pizza.sol";

contract PizzaV2 is Pizza {
   ///@dev increments the slices when called
   function refillSlice() external {
       slices += 1;
   }

   ///@dev returns the contract version
   function pizzaVersion() external pure returns (uint256) {
       return 2;
   }
}

N.B., the PizzaV2 contract inherits from the Pizza contract. So all the functions including the two newer functions will be present in the V2 contract.

Once the file is saved, now we can upgrade our Pizza contract to PizzaV2. Inside the scripts directory, create a new file upgrade_pizza_v2.js, add the following contents, and save the file. It is responsible for upgrading the deployed contract:

const { ethers, upgrades } = require("hardhat");

const PROXY = <<REPLACE_WITH_YOUR_PROXY_ADDRESS>>;

async function main() {
 const PizzaV2 = await ethers.getContractFactory("PizzaV2");
 console.log("Upgrading Pizza...");
 await upgrades.upgradeProxy(PROXY, PizzaV2);
 console.log("Pizza upgraded successfully");
}

main();

You can run the following command to execute the upgrade:

npx hardhat run ./scripts/upgrade_pizza_v2.js –network kovan

Executing The Upgrade

N.B., if you face any errors when running the above command, retry it two or three times. It should work.

If you check Etherscan, you can see there’ll be another two transactions from the deployer wallet. The first one is the deployment of the PizzaV2 contract and the second transaction will be the upgradeTo call in the Pizza contract to perform an upgrade. This makes sure that the proxy contract points to the newly deployed PizzaV2 contract.

You can verify the PizzaV2 contract from the terminal by running:

npx hardhat verify –network kovan <<PIZZA_V2_ADDRESS>>

If you check the Write as Proxy tab inside the Contract tab of the proxy contract in Etherscan, you can see the newly created view method — refillSlice() — along the older methods. Also, there will be a pizzaVersion() method in the Read as Proxy tab, which confirms that the upgrade is successful!

Confirming The Upgrade Using The Read As Proxy Tab

Whoa. 🎉 🎉 We’ve successfully deployed and upgraded contracts using the UUPS proxy pattern!

Closing thoughts

Though there are several advantages to the UUPS pattern, and the recommended proxy pattern is currently the UUPS pattern, there are some caveats that we should be aware of before implementing this into a real-world project.

One of the main caveats is that because the upgrades are done via the implementation contract with the help of upgradeTo method, there’s a higher risk of newer implementations to exclude the upgradeTo method, which may permanently kill the ability to upgrade the smart contract. Also, this pattern is a bit complex to implement when compared to other proxy patterns.

Despite the warnings, UUPS is a very gas-efficient proxy pattern that has several advantages. The code for this project along with some unit tests can be found in the GitHub repo here. Feel free to play around with the code.

Happy coding! 🎊

The post Using the UUPS proxy pattern to upgrade smart contracts appeared first on LogRocket Blog.



from LogRocket Blog https://ift.tt/1dxqCWX
via Read more

Post a Comment

0 Comments
* Please Don't Spam Here. All the Comments are Reviewed by Admin.
Post a Comment

Search This Blog

To Top