All you need to know about UPGRADABLE Smart Contracts

One of the greatest features of the blockchain is that data are immutable. That means that nobody can change the content of any transaction without invalidating the whole blockchain.

But unfortunately, there are several issues with this system:

  • When you deploy your smart contract, you can’t change the code afterwards and thus can’t upgrade the smart contract. Once the transaction is sent: it stays forever, there is nothing you can do to reverse the state previous state of the EVM.

This is very cumbersome process, it takes a lot of time and there is a high chance of making mistakes when migrating the data which can lead to disastrous consequences to users.

However, the good news is: you can circumvent this issue, by using upgradable smart contract and this is the main topic of his article !

1. How it works ?

The system is quite simple, you’ll need 2 different smart contracts.

  1. The first one is called the implementation (or the logic), it contains all the regular functions of the smart contract. (like transfer(), approve(), and so on…)

When you call (for example) transfer in the proxy contract, the proxy will “delegatecall()” transfer in the implementation contract.

If you don’t know what delegatecall is: https://solidity-by-example.org/delegatecall/

This means that the transfer() function (initially written in the implementation contract) will be executed in the proxy contract context. (The difference with regulars smart contracts is that the storage is situated in the proxy contract but the function in implementation contract)

It’s very useful because when you want to upgrade the smart contract:

You just need to set the address of the implementation in the proxy contract to the new implementation/logic contract.

But the storage is still in the proxy, as a result it’s still here and above all: The storage is NOT LOST (if we stored the data in the implementation contract then when we will update to a new implementation contract the storage will be reset.)

And… it’s all! When all these operations are done you can discard the former smart contract and start using the new like before.

Any user calling the smart contract, won’t see any difference apart from the functions updated. Done!

2. There is 3 different proxy patterns

These are several ways to organize upgradable contracts by using more or less complex systems.

2.1 The first way is called: Transparent Proxy pattern.

This is the simplest possible proxy patterns, it behaves like described above in the first section. Here is the code:

contract proxy{   
address _impl;
// address to implementation.
function upgradeTo(address newImpl) external {
_impl = newImpl;
}

fallback() {
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 returned data
returndatacopy(ptr, 0, size)
// (4) forward return data back to caller

switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}

}
// and others functions
}

Every time the user calls a function in the proxy, the fallback function delegate all parameters to the implementation contract and return the result. (revert if the result is zero.)

If we want to upgrade the contract, we just need to deploy the new smart contract and change the implementation in the proxy to the smart contract by call the function upgradeTo which set the implementation. (don’t forget to add an owner to the contract otherwise everyone will be able to upgrade the contract with their own code)

2.2 The second one is the most popular: it’s called UUPS proxy. (or ERC1967 proxy)

A 3rd smart contract is added: the ProxyAdmin contract which controls the “public proxy”. (The proxy which delegates to the implementation.)

It contains some admins function, to ensure that only a small set of addresses can upgrade the proxy.

2.3 The third one is diamond proxy, which is a lot more complex but blow up almost all limitations of the UUPS upgradable proxies (limitation which we will see a bit later)

  • The storage and the functions are splitted up between 2 differents contracts. (minimum)

This is suitable for very large decentralized applications, but in our case it’s very complicated, so in the next sections we will be more focused on the UUPS proxys which is suitable for 98% of DApps.

3. Upgradable proxy tutorial

In this section, I’ll show you how to use UUPS upgradable proxy with hardhat.

I assume that you already know how to create and deploy a smart contract with this tool, if not visit https://hardhat.org/.

There is 2 quick Steps for the installation:

  1. Installation.
npm install — save-dev @openzeppelin/hardhat-upgrades npm install — save-dev @nomiclabs/hardhat-ethers ethers 

2. Registering the new plugin in the hardhat.config.js

require(‘@openzeppelin/hardhat-upgrades’);

Now for the example we will write a test smart contract :

//SPDX-License-Identifier: Unlicense 
pragma solidity ^0.8.0;
contract MyContract {
function returnValue() public view returns(uint) {
return 1;
}
}

The contract contains just one function which returns always 1.

Our objective is to upgrade this contract which will always return 2.

There is 2 steps.

1) Create the script for deploying the proxy and the first version of the smart contract (in scripts/deploy.js)

const { ethers, upgrades } = require("hardhat");  async function main() {   
const Box = await ethers.getContractFactory("MyContract");
// get the MyContact which return 1
const box = await upgrades.deployProxy(Box);
// deploy the proxy and the contract
await box.deployed();
console.log("Box deployed to:", box.address);

// show the contract address once it's deployed
}

Don’t forget to run scripts/deploy.js

 : npx hardhat run --network [your_network] scripts/deploy.js

2) Create the script for updating smart contract (scripts/update.js)

const { ethers, upgrades } = require("hardhat");  
let BOX_ADDRESS = "..."
// set BOX_ADDRESS to box.address, the address of the proxy.
async function main() {
const BoxV2 = await ethers.getContractFactory("MyContractV2");
// get the new contract (version 2)
const box = await upgrades.upgradeProxy(BOX_ADDRESS, BoxV2);
// set proxy's impementation to BOXV2
console.log("Box upgraded");
// when it's done it prints "box upgraded".
}

Here is the contract MycontractV2, the only difference with the first version, is that the function returns always 2.

//SPDX-License-Identifier: Unlicense 
pragma solidity ^0.8.0;
contract MyContract {
function returnValue() public view returns(uint) {
return 2;
}
}

And it’s done, every time you want to upgrade your smart contract, just run the update.js script with the news contract in the variable BOX_ADDRESS. (which need to be the proxy address)

npx hardhat run — testnet testnet scripts/update.js

Hardhat will do the rest, writing upgradable smart contract, have never been easier!

Important Rules.

Warning: Beware that using proxy comes at a certain cost, in fact there is some slight limitations to UUPS upgradable contracts:

1) The constructor must be empty

In programming languages the constructor is always executed one the first time to initiate the parameters, but when we upgrade smart contracts, the constructor is executed another time which can lead to undefined behavior. We need to guarantee that all the parameters all initialized one time.

In place of the constructor, we’ll use an initializer function with a initializer modifier

//dangerous constructor : 
constructor(uint256 _x,_y) { x = _x y = _y }
//should become
function initialize(uint256 _x,uint256 _y) public initializer { x = _x y = _y }
modifier initializer() {
require(!initialized, "Contract instance has already been initialized");
initialized = true; _;
}

When initialize is called one time, the initializer modifier mark “initialized” as “true” and prevent initialize to be called a second time .

Info : when you deploy the proxy and the contract for the first time, hardhat call initialize() directly, so you don’t need to do it by your own

2) You can’t delete variables, you can’t change the type and the order of the variables too.

Why? Because this will reorder the slots in the EVM storage and make the smart contract unusable for example:

uint a = 11 //slot 1uint b = 22 //slot 2uint c = 33 //slot 3

If we reorder b and c in the upgrade (V2 of the smart contract), we get

uint a // slot 1uint c // slot 2uint b // slot 3

The slot 2 and the slot 3 won’t be reordered in the proxy, where data is stored. Slot 2 will still be equal to 22 instead of 33 so, c will be equal to 22.

Slot 3 will still be equal to 33 instead of 22 so, b will be equal to 33.

Similar issues are happened when a variable is deleted. (Slot in the EVM are not freed.)

In fact any upgrade that change the order in the EVM storage can endanger data.

This is known as a storage clash vulnerability.

3) You can’t use external libraries.

These limitations impose that a lot of libraries won’t be compatible with upgradable contracts.

To troubleshoot this issue, Openzeppelin has rewritten a lot of their libraries to be “upgrade-safe” be compatible with the rules defined above.

So it’s possible to write upgradable ERC20, ERC721, ERC1155…. contracts.

For that, you need to install additionally: @openzeppelin/contracts-upgradable

Contracts are written here: https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable

They are others limitations, you can learn about here: https://docs.openzeppelin.com/upgrades-plugins/1.x/faq

4. Conclusion

I hope you enjoyed my tutorial about smart contracts upgrades. See you in a next tutorial!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
TrustChain

Smart contract Auditor & Cybersecurity engineer, follow me on Twitter to get more value: https://twitter.com/TrustChain_DEFI