How to write an Upgradeable ERC721 NFT Contract

Deepti Nagarkar
deeblueangel
Published in
5 min readApr 25, 2022
Photo by olieman.eth on Unsplash

NFTs or non-fungible tokens are the latest trend. Everyone wants to mint an NFT and get that elusive following. This tutorial will cover how to write an upgradeable ERC 721 contract on Ethereum.

What are Upgradeable Contracts ?

Smart Contracts on Ethereum Blockchain are by their design : immutable, which means that once coded and deployed, they cannot be changed or edited. This provides the trust, security, transparency and reliability that blockchains are known for. However, what happens when a bug is discovered in the said contract? As is the nature of software, it needs to be editable for new features and patches with bug fixes. With immutable smart contracts, one would need to deploy a new contract with the changes and then ask the existing users to start using the new contract. This is fine for a small user base, but rather unwieldy for large user base and also inconsistent as few may choose not to upgrade their contracts.

Upgradeable contracts to the rescue. The concept is simple. The actual contract sits behind another contract, called the proxy contract. The users interact with this proxy contract address. what it means that the user always interacts with the same contract (proxy), but the underlying logic can be changed (upgraded) whenever needed without losing any previous data.

Pre-requisites

  • A working ERC721 Contract — example given on Alchemy (please refer to this for setting up the development environment as well.)
  • Working knowledge of coding and deploying an ERC721 Contract on Ethereum Testnet
  • Metamask plugin with account created on Rinkeby network with preloaded test ether

For the purposes of this example, refer to the NFTCollectible.sol contract file here. The upgradeable pattern that is going to be used it Universally Upgradeable Proxy Standard (UUPS) .

Basic Project Structure —

  • Create an empty folder —

mkdir UpgradeableNFT

  • In the folder directory run the following command. It will initialize the npm package.json —

npm init -y

  • Next install hardhat with the following command —

npm install hardhat — save-dev

  • Run hardhat with the following command and select Create Simple Project from the options given.

npx hardhat

  • Copy the NFTCollectible.sol contract file into the contracts folder
  • Create a .env file in the project root folder and enter the following variables —

API_URL = “https://eth-rinkeby.alchemyapi.io/v2/<<Alecmhy API KEY>>"
PRIVATE_KEY = “<<Metamask Account Private Key >>”
ETHERSCAN_API = “<<Etherscan API Key>>”

  • Edit the hardhat.config file and add the following content to it.

Make the Contract Upgradeable

Now lets get started with making the contract upgradeable —

Step 1 — Install the OpenZeppelin Upgrades Plugin along the hardhat dependencies with the following command

npm i @openzeppelin/contracts-upgradeable @openzeppelin/hardhat-upgrades — save-dev

Step 2 — OpenZeppelin Upgrades Plugin, provides corresponding upgradeable versions for each of the of the classes that are inherited from. Open the NFTCollectible.sol contract and make the following changes —

Step 2.1— Add the following import statements

// 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”;

import “@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol”;

Step 3 — Edit the contract to inherit from UUPSUpgradeable, OwnableUpgradeable and ERC721EnumerableUpgradeable.

Original Contract v/s Upgradeable Contract

Step 4 — Upgradeable Contracts cannot have constructors since they are initialized in the context of the proxy contract. Delete the constructor and add a initialize method as below —

//Replace Constructor with initialize
// constructor(string memory baseURI) ERC721(“NFT Collectible Collection”, “BlueNFT”) {
// setBaseURI(baseURI);
//}

Add the below method instead of the constructor

//Ensures contract is initialized only once
function initialize(string memory baseURI) public initializer {
__ERC721_init(“BlueNFT Collectible Collection V1”,”BlueNFT”);
__Ownable_init();
__UUPSUpgradeable_init();

setBaseURI(baseURI);

}

Step 5 — Override the authorizeUpgrade method as below. This ensures that only the contract owner can upgrade the contract.

//UUPS module required by openZ — Stops unauthorized upgrades
function _authorizeUpgrade(address) internal override onlyOwner{

}

That’s all the changes required in the contract.

Step 6 — Create a deploy_v1.js file in the scripts folder of the project. Add the following content to it.

Step 7 — Deploy the contract to the rinkeby network with the following command

npx hardhat run ./scripts/deploy_v1.js — network rinkeby

Step 8 — Search for the contract on Etherscan Rinkeby Testnet. Once the contract is located, click on the “More Options” as shown in screen shot below and select “Is this a Proxy?” Option.

It will then ask to verify the code for the contract. Copy the address given in the popup box. That is the actual contract address.

Step 9 — Verify the contract code by typing the following command in the terminal of the project.

npx hardhat verify — network rinkeby <<Contract Address>>

Step 10 — Repeat Step 8 and go ahead and point the proxy to the verified contract. Note the Proxy address of the deployed proxy. This is required later in step 12.

Step 11 — Now, suppose the contract is to be upgraded to Version V2. For the sake of simplicity we are just going to add a new method which prints the version number. Create a new contract file “NFTCollectibleV2.sol” in the contracts folder. Add the following content to it —

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

import “./NFTCollectible.sol”;

contract NFTCollectibleV2 is NFTCollectible {

function getVersion() external view returns(string memory){
string memory version = “This is version 2”;
return version;
}

}

Step 12 — Create a deploy_v2.js file in the scripts folder and add the following content to it.

Note the Proxy address of the deployed proxy and replace it in the deploy_v2.js file (<<ADDRESS OF THE PROXY>>).

Step 13 — Repeat Step 7 to Step 9. Now the Read as Proxy Function of the Proxy Contract should have an additional method “getVersion()”. Note the command to deploy the new contract

npx hardhat run ./scripts/deploy_v1.js — network rinkeby

And its done! The Contract has been upgraded. Inheritance was used in this example to add to additional functionality to an existing contract. One can override functions of parent contract to add new functionality as well. Alternatively, a new contract which does not inherit from the earlier contract can also be added. But that would have to ensure to either have backward compatibility to the original contract or intimate the users about the changed methods and request them to consume the new methods.

--

--