Why?
You might ask why do we need upgrades and isn’t that something that completely violates the immutability of Blockchain and Smart Contracts?In a way, yes. Still, we need upgrades, reason is, imagine that you set a mint fee for your NFT SC (Smart Contract) that is equal to 5$ worth of ETH and then you wanted to change that value to 10$ worth of ETH. Why, inflation, demand or anything else. Not just fees, let’s say that you are creating a game that has some player types and you wanted to add a new player type, what will happen then?
Also one more violation is decentralization right? What if the contract upgraded with some kind of bad code, whoever has the ability to upgrade the SC has control over everything with the upgrades right? Centralizing might not be a problem for some cases but what if decentralization is important part of that contract’s purpose? There comes Multisig Upgrades which is basically not having a single entity or wallet who controls the upgrades. First you have to propose an upgrade and the SC will be upgraded only after the owners of Multisig reviews and approves the proposal. Read more about Upgrading via Multisig with Openzeppelin
Approaches
Setter Functions
This is basically defining the values in the SC and changing those values via setter functions. In below code, imagine that we want 0.00005 ether
as fee for every request to one of our function and after some time I wanted to change that value to
0.01 ether
uint256 fee = 0.00005 ether;
function setFee(uint256 _fee) public {
fee = _fee;
}
But of course there are drawbacks for this. First of all, you have to foresee everything that you might need from the start. Second, you can’t add any new function or change the functionalities of existing functions. So this is more of an update than upgrade.
Contract Migration
This is basically deploying a completely new contract and transferring old data (storage, balances etc.) from previous contract to that newly deployed contract. If you have a large amount of data, you will have to split the migration into multiple transactions and with that, can incur high gas costs.This approach has also other drawbacks, of course the first one is transferring the data. Then you have to announce and convince people to use this new contract and say that this is the new address, then depending on the contract you have to contact exchanges or people who are using your contract and also let them know of your new contract address.
Data Separation
Using this approach we can separate our logic and the storage into different contracts. Clients are going to call the logic contract always, logic contract has the address of the storage contract so only logic contract interacts with the storage contract.In this approach, storage contract is always immutable. Only logic contract is replaced with a new implementation. So the storage (user balances etc.) stays the same but you can add new functionality or modify the existing ones without the need of a data migration. Also remember to change logic contract’s address in storage contract and to add an authorization layer so only logic contract can call the storage contract.
Proxy
First of all, if you don’t know what a Proxy is, without going into detail, Proxy is something that acts as gateway or a middleman, that sits between things like client/server, browser/server … and client/smart contract in our case.
![](https://wpstg.cloudoki.dev/wp-content/uploads/2023/03/1_1CyJDi4HSDPnQ0IDWwvRrw.png)
In our case, basically we are not only deploying our contract but we also deploy a Proxy contract which will forward calls to the correct (latest version) implementation of the real contract, to do that it uses some low level functionality like delegating calls.
delegatecall
executes other contract’s code inside the contract that called it. msg.value
and msg.sender
also doesn’t change in contract that the call is delegated.
Now we will call this Proxy contract instead of directly calling our contract.
![](https://wpstg.cloudoki.dev/wp-content/uploads/2023/03/1_pnML1mGLzn-cfpCpWDQzdw.png)
See the basic code for this kind of operation:
assembly {
let ptr := mload(0x40)
// (1) copy incoming call data
calldatacopy(ptr, 0, calldatasize)
// (2) forward call to logic contract
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
let size := returndatasize
// (3) retrieve return data
returndatacopy(ptr, 0, size)
// (4) forward return data back to caller
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
Of course this also comes with couple of problems, one of them is storage collisions but this is not something that we need to worry about if we’re using Openzeppelin, if not and you have your own Proxy contract then you can approach that problem in the same way that Openzeppelin does. Read more about storage collisions in Proxy contracts.
Another problem with Proxy approach is the function clashes. I will not go into detail yet you can read this story about function clashes in SC with Proxy approach for more details. Again Openzeppelin contracts handle function clashes for us, we’ll speak about how in a moment.
Now let’s see several different Proxy patterns.
Transparent Upgradeable Proxy
This is a proxy with a built in admin and upgrade interface. With this pattern, admin can only call admin functions in the Proxy and can never interact with the implementation contract.So if you are the admin of the contract and also want to interact with the implementation contract, that’s not possible. You have to interact with a different wallet.
modifier ifAdmin() {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback();
}
}
This also avoids the function clashes because non admin users will always call _fallback
and never be able to call Proxy Admin.
![](https://wpstg.cloudoki.dev/wp-content/uploads/2023/03/1_RalC5_9KPVQtmvgHxLUpFA.png)
UUPS (Universal Upgradeable Proxy Standard)
This is very similar to Transparent Upgradeable Proxy but instead of having the upgrade logic in the Proxy, it’s in the implementation contract. UUPS proxies rely on an _authorizeUpgrade
function to be overridden to include access restriction to the upgrade mechanism.
Benefits of UUPS over Transparent Proxy:
- Since the upgrade functionalities are now in the implementation contract Solidity compiler can detect function clashes.
- It’s gas efficient since there is no need for
ifAdmin
modifier anymore - Flexibility to remove upgradeability. Removing upgradeability is good but it may not be good if you forget to add upgrade logic to new implementation, resulting that upgradeability will be lost in the new contract.
Read more about Transparent vs UUPS Proxies
Diamond Proxy
Diamond Proxy allows us to delegate calls to more than one implementation contract, known as facets, similar to microservices. Function signatures are mapped to facets.
![](https://wpstg.cloudoki.dev/wp-content/uploads/2023/03/1_9I2Y34iLmhc0Y-PhvXazkA.png)
mapping(bytes4 => address) facets;
Code of call delegation is very similar to the ones that UUPS and Transparent Proxies using but before delegating the call we need to find the correct facet address:
// Find facet for function that is called and execute the
// function if a facet is found and return any value.
fallback() external payable {
// get facet from function selector
address facet = selectorTofacet[msg.sig];
require(facet != address(0));
// Execute external function from facet using delegatecall and return any value.
assembly {
// copy function selector and any arguments
calldatacopy(0, 0, calldatasize())
// execute function call using the facet
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
// get any return value
returndatacopy(0, 0, returndatasize())
// return any return value or error back to the caller
switch result
case 0 {revert(0, returndatasize())}
default {return (0, returndatasize())}
}
}
Benefits of Diamond Proxy:
- All smart contracts have a 24kb size limit. That might be a limitation for large contracts, we can solve that by splitting functions to multiple facets.
- Allows us to upgrade small parts of the contracts (a facet) without having to upgrade the whole implementation.
- Instead of redeploying contracts each time, splitted code logics can be reused across different Diamonds.
- Acts as an API Gateway and allows us to use functionality from a single address.
Upgradeable ERC721 Contract with Hardhat
You can easily set up a Hardhat project from here: https://hardhat.org/hardhat-runner/docs/getting-started
First of all let’s create different versions of our NFT Contract. Create MyNFTV1.sol
under the contracts directory.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFTV1 is ERC721URIStorageUpgradeable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
function initialize() public initializer {
__ERC721_init("MyNFT", "MYN");
}
function mintItem(string memory tokenURI) public payable returns (uint256) {
require(
msg.value >= 0.00001 ether,
"You have to pay at least 0.00001 ether to mint."
);
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(msg.sender, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
function latestTokenId() public view returns (uint256) {
return _tokenIds.current();
}
}
Now it’s time to mention that we do not have constructor
functions in our upgradeable contracts. Reason is, constructor functions runs immediately when the contract is deployed. We don’t want that, because we first deploy our contract and only then update the implementation address in the Proxy. So we need the initialize
to run once we have upgraded the contract.
As you can see in V1 of our ERC721 contract we have a view function named latestTokenId
and a mint
function that takes a 0.00001 ether
as fee.
Now let’s modify main
function in the deploy.ts
under the scripts directory and then run.
async function main() {
const MyNFT = await ethers.getContractFactory('MyNFTV1');
const myNFT = await upgrades.deployProxy(MyNFT);
await myNFT.deployed();
// try to mint the item with required fee
try {
await myNFT.mintItem('https://run.mocky.io/v3/17d6e506-17e3-4b53-a964-a7e0ed565ad0', { value: ethers.utils.parseEther('0.000001') });
} catch (err: any) {
console.log(err.reason);
}
// get the latest token id that is minted
console.log(await myNFT.latestTokenId());
}
![](https://wpstg.cloudoki.dev/wp-content/uploads/2023/03/1_K0CjkaG98ctKPLu-Ykr63Q-1024x154.png)
(Ignore the warnings I am using a newer version of Nodejs that Hardhat does not support yet) Transaction is reverted because we sent 0.000001 ether
instead of 0.00001 ether
. Now let’s try with the correct value now.
![](https://wpstg.cloudoki.dev/wp-content/uploads/2023/03/1_awhjTpc7ipUgwgkyJKxXnw-1024x143.png)
Great we got the correct token id now. Let’s create MyNFTV2.sol
under the contracts directory.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFTV2 is ERC721URIStorageUpgradeable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
function initialize() public initializer {
__ERC721_init("MyNFT", "MYN");
}
function mintItem(string memory tokenURI) public payable returns (uint256) {
require(
msg.value >= 0.00005 ether,
"You have to pay at least 0.00005 ether to mint."
);
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(msg.sender, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
function latestTokenId() public view returns (uint256) {
return _tokenIds.current();
}
function nextTokenId() public view returns (uint256) {
uint256 current = _tokenIds.current();
return current + 1;
}
}
Difference of V2 than V1 is our fee now 0.0005 ether
and we have a new view function named nextTokenId
now let’s update our deploy script to deploy a fresh V1 contract, mints an NFT and then upgrade to V2 contract and mints another NFT and we need to see that our latest token id is 2 and be able to call nextTokenId
async function main() {
// deploy a new contract
const MyNFTV1 = await ethers.getContractFactory('MyNFTV1');
const myNFTv1 = await upgrades.deployProxy(MyNFTV1);
await myNFTv1.deployed();
try {
console.log(await myNFTv1.nextTokenId());
} catch (_) {
console.log(`nextTokenId function is not defined in version 1.`);
}
try {
await myNFTv1.mintItem('https://run.mocky.io/v3/17d6e506-17e3-4b53-a964-a7e0ed565ad0', { value: ethers.utils.parseEther('0.00001') });
} catch (err: any) {
console.log(err.reason);
}
// upgrade the contract
const MyNFT = await ethers.getContractFactory('MyNFTV2');
const myNFT = await upgrades.upgradeProxy(myNFTv1.address, MyNFT);
await myNFT.deployed();
console.log(`MyNFT deployed to ${myNFT.address}`);
// try to mint the item with required fee
try {
await myNFT.mintItem('https://run.mocky.io/v3/17d6e506-17e3-4b53-a964-a7e0ed565ad0', { value: ethers.utils.parseEther('0.00005') });
} catch (err: any) {
console.log(err.reason);
}
console.log(await myNFT.latestTokenId());
console.log(await myNFT.nextTokenId());
}
![](https://wpstg.cloudoki.dev/wp-content/uploads/2023/03/1_GuCXQyowQSVMP-dHbRc8Tg-1024x201.png)
Awesome, it works! You can see the codes for this in my GitHub https://github.com/dogukanakkaya/erc721-upgradeable it also contains the tests and config for Polygon Mumbai network so you can try this there also.
Related Articles
![Blockchain Development](https://cyrexenterprise.com/wp-content/uploads/2025/01/Guide-to-Blockchain-Development-in-2025.webp)
Your Guide to Blockchain Development in 2025
Blockchain Development in 2025: Explore the latest trends in Web3, interoperability, and d...
Read more![Cyrex Enterprise](https://cyrexenterprise.com/wp-content/uploads/2024/12/CHRISTMAS-BLOG.webp)
Reflecting on 2024: A Year of Growth, Innovation, and Milestones
Reflect on 2024 with Cyrex Enterprise! Discover our achievements in software development, ...
Read more![](https://cyrexenterprise.com/wp-content/uploads/2024/10/Engineer-Write-up.webp)
Deploying NestJS Microservices to AWS ECS with Pulumi IaC
Let’s see how we can deploy NestJS microservices with multiple environments to ECS using...
Read more![CI/CD](https://cyrexenterprise.com/wp-content/uploads/2024/09/What-is-CICD.webp)
What is CI/CD? A Guide to Continuous Integration & Continuous Delivery
Learn how CI/CD can improve code quality, enhance collaboration, and accelerate time-to-ma...
Read more