UUPS Proxies: Tutorial (Solidity + JavaScript)

UUPS Proxies: A Tutorial

In this tutorial we will deploy an upgradeable contract using the UUPS proxy pattern. We assume some familiarity with Ethereum upgradeable proxies.

Introduction to UUPS

The original proxies included in OpenZeppelin followed the Transparent Proxy Pattern. While this pattern is still provided, our recommendation is now shifting towards UUPS proxies, which are both lightweight and versatile. The name UUPS comes from EIP1822, which first documented the pattern.

While both of these share the same interface for upgrades, in UUPS proxies the upgrade is handled by the implementation, and can eventually be removed. Transparent proxies, on the other hand, include the upgrade and admin logic in the proxy itself. This means TransparentUpgradeableProxy is more expensive to deploy than what is possible with UUPS proxies.

UUPS proxies are implemented using an ERC1967Proxy. Note that this proxy is not by itself upgradeable. It is the role of the implementation to include, alongside the contract's logic, all the code necessary to update the implementation's address that is stored at a specific slot in the proxy's storage space. This is where the UUPSUpgradeable contract comes in. Inheriting from it (and overriding the _authorizeUpgrade function with the relevant access control mechanism) will turn your contract into a UUPS compliant implementation.

Let's delve into the details.

OpenZeppelin Tooling

We will use OpenZeppelin tooling to easily and securely deploy an upgradeable contract using the UUPS proxy pattern.

  1. We will write our contract source code using OpenZeppelin Upgradeable Contracts.
  2. We will deploy this contract behind a proxy using OpenZeppelin Upgrades Plugins, which additionally provides security checks to make sure we respect some basic rules of upgradeability.

Project Setup

In a new directory, start an npm project and install Hardhat along with the OpenZeppelin tools we mentioned in the previous section:

npm init -y
npm install hardhat @nomiclabs/hardhat-ethers ethers
npm install @openzeppelin/contracts-upgradeable @openzeppelin/hardhat-upgrades

Create the Hardhat config set to use Solidity 0.8.2 and the plugins we've installed:

//  hardhat.config.js

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

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.3",
};

Solidity 0.8.2 is technically supported, but the Windows build of the compiler currently has an unresolved issue, so 0.8.3 is recommended.

Writing the Contract

We will start with a basic ERC20 token with some initial supply minted for the deployer. We will see below that this contract is not yet ready for UUPS deployment.

// contracts/MyTokenV1.sol

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

contract MyTokenV1 is Initializable, ERC20Upgradeable {
    function initialize() initializer public {
      __ERC20_init("MyToken", "MTK");

      _mint(msg.sender, 1000 * 10 ** decimals());
    }

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}
}

Because this is a upgradeable contact we do not initialize the contract state in the constructor, and instead we have an initializer function decorated with the modifier initializer, provided by Initializable. Because of this requirement, we also made sure to use the Upgradeable variant of OpenZeppelin Contracts, which similarly provides contracts with initializer functions such as __ERC20_init, all following the same naming convention.

Upgradeable Deployment

We can now write a test and start by deploying this contract normally:

// test/MyToken.test.js

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

describe('MyToken', function () {
  it('deploys', async function () {
    const MyTokenV1 = await ethers.getContractFactory('MyTokenV1');
    await MyTokenV1.deploy();
  });
});

Run the test using npx hardhat test, and you should see it run successfully.

  MyToken
    ✓ deploys (923ms)

In order to make this an upgradeable deployment, we need to use the function deployProxy from the Upgrades plugin.

    await upgrades.deployProxy(MyTokenV1); // instead of MyTokenV1.deploy()

This function will be doing several things behind the scenes. The contract will be checked for unsafe patterns that shouldn't be used in an upgradeable deployment, such as using the selfdestruct operation. If these checks pass, it will then deploy the implementation contract, and then deploy a proxy connected to that implementation. The result is a contract that can then be upgraded using upgrades.upgradeProxy as we will see later.

The proxy we just deployed was not a UUPS proxy, however, as the current default is still Transparent Proxies. In order to use UUPS we have to manually specify so with the option kind: 'uups'.

    await upgrades.deployProxy(MyTokenV1, { kind: 'uups' });

If we try to run this test now, we will actually see it fail with this error.

contracts/MyTokenV1.sol:7: Implementation is missing a public `upgradeTo(address)` function
    Inherit UUPSUpgradeable to include this function in your contract
    @openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol
    https://zpl.in/upgrades/error-008

This is because, as we mentioned at the beginning of the tutorial, UUPS proxies do not contain an upgrade mechanism on the proxy, and it has to be included in the implementation. We can add this mechanism by inheriting UUPSUpgradeable, and this will also require that we implement an authorization function to define who should be allowed to upgrade the contract. For the example we will use Ownable.

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
 
contract MyTokenV1 is Initializable, ERC20Upgradeable, UUPSUpgradeable, OwnableUpgradeable {
    function initialize() initializer public {
      __ERC20_init("MyToken", "MTK");
      __Ownable_init();
      __UUPSUpgradeable_init();

To authorize the owner to upgrade the contract we implement _authorizeUpgrade with the onlyOwner modifier.

    function _authorizeUpgrade(address) internal override onlyOwner {}
Click here to see the full Solidity code.
// contract/MyTokenV1.sol

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract MyTokenV1 is Initializable, ERC20Upgradeable, UUPSUpgradeable, OwnableUpgradeable {
    function initialize() initializer public {
      __ERC20_init("MyToken", "MTK");
      __Ownable_init();
      __UUPSUpgradeable_init();

      _mint(msg.sender, 1000 * 10 ** decimals());
    }

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}

    function _authorizeUpgrade(address) internal override onlyOwner {}
}

Now that the contract is done we can run our test to deploy a UUPS proxy.

Click here to see the full test code.
// test/MyToken.test.js

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

describe('MyToken', function () {
  it('deploys', async function () {
    const MyTokenV1 = await ethers.getContractFactory('MyTokenV1');
    await upgrades.deployProxy(MyTokenV1, { kind: 'uups' });
  });
});

Once we have a new version of the contract code and we want to upgrade a proxy, we can use upgrades.upgradeProxy. It's no longer necessary to specify kind: 'uups' since it is now inferred from the proxy address.

await upgrades.upgradeProxy(proxyAddress, MyTokenV2);

More Resources

You can find an updated list of resources related to upgradeability in the Upgrades page of the documentation.

24 Likes

Thanks for the tutorial!

Quick question, is there a tutorial on how to verify the contract code on Etherscan/Polyscan for the deployed contracts?

Hi, welcome! :wave:

I think you can have a look at this tutorial: Verify smart contract inheriting from OpenZeppelin Contracts - General / Guides and Tutorials - OpenZeppelin Community

As for Polyscan, I am not sure.

1 Like

Thanks for the reply. I was able to verify my implementation contract (since I have the source code for that). However, as for the Proxy contract, I’m not sure how or what to retrieve so that I can flatten to submit to Etherscan/Polyscan for verification.

I tried flattening various files in the locally installed proxy folder (https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/tree/master/contracts/proxy). Nothing seems to work or has the matching bytecode as the code that was deployed using the
await upgrades.deployProxy(ERCToken, { kind: 'uups' }); command.

1 Like

Can you share the address of the proxy contract you’re trying to verify?

2 Likes

Here’s the address on Polygon Testnet: https://mumbai.polygonscan.com/address/0xa4059faa4f829564fd08943ef1a1bd574286038f

I tried the same thing on Rinkeby and it seems like the ERC1967Proxy contract source code is already verified on Rinkeby.

1 Like

I’ve verified your contract now. We currently rely on manual verification once for each network (afterwards it’s cached by Etherscan/Polygonscan). The Solc JSON Input can be obtained by building the contracts in the repository (https://github.com/OpenZeppelin/openzeppelin-upgrades) if anyone is interested to know.

Soon this should be automated.

4 Likes

Thank you for verifying it on Mumbai testnet! Looking forward to see this get automated. :+1:

Hi,
Is there a way to kill upgradeability without using selfdestruct?. From 'https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups' there is a clause that says "While both of these share the same interface for upgrades, in UUPS proxies the upgrade is handled by the implementation, and can eventually be removed." How can i permanently kill upgrade functionality? considering you cannot use selfdestruct.

What it means is that a subsequent implementation can omit the upgradeTo function, and this renders the UUPS proxy not upgradeable anymore.

This situation is not easily allowed by our UUPSUpgradeable contract as it will verify that upgradeability is preserved. But you can achieve it by first upgrading to a contract that contains a custom upgradeTo function (can be ERC1967Upgrade._upgradeTo), and then upgrading again to a contract that no longer has the upgradeTo function.

Thanks Frangio. :+1:

2 posts were split to a new topic: Deploy on Kovan fails while Ganache succeeds

Hi, Thanks for the great tutorial!
I tried to verify my ERC20 token contract that created with UUPS, but it did not work, I ran this command:
npx hardhat verify --network rinkeby 0x6957a76BA2C12359E1Ea0F75Ec302F0c27ba4972
Any ideas for this? Thanks!

You are verifying the proxy addess u need to verify the implementation address and on etherscan u need to point proxy address to implementation addess once verification of implementation of address is done

@18dew Thanks, it works!
Quick question, with UUPS pattern, let's say I have Contract1, and then Contract2 is Contract1
, and then Contract3 is Contract1. As I understand Contract3 must be declared all the variables from Contract2, so now we are at Contract3 but there are some unused variables in Contract2 we must declare in Contract3. Is that a problem? and is there a solution for that?

@David_Hoang Can you create a new post and share some examples of code to describe this situation?

@frangio Yes, sure
Since I'm not really sure if this is an issue yet so I think I reply here first

//SPDX-Lincense-Identifier: MIT
pragma solidity ^0.8.0;
contract Contract1  {
	uint ct1data;

}

contract Contract2 is Contract1{
	uint ct2data;
}

//and now I dont wanna use ct2data variable so new version:

contract Contract3 is Contract1 {
	// this one must be declared here otherwise error with storage layout
	// seems an issue?
	// as later on we may have so many ununsed variables?
	uint ct2data; 
	uint ct3data;

}```

If I understand this correctly, the the proxy contract which is auto-generated is identical to the contract I write as far as the storage (variables etc.) is concerned?

I'm assuming this would initially deploy Contract1, then upgrade it to Contract2, then upgrade it to Contract 3.

Yes, in this scenario Contract3 has to redeclare ct2data. Otherwise, ct3data would be allocated in the place where ct2data had been before, and this potentially corrupts the storage.

This can result in unused variables. You can add a comment saying that the variable is unused and only kept for storage compatibility.

1 Like

The proxy contract is always the same for all contracts, and doesn't have storage variables in the Solidity code of the proxy.

But the proxy instance will share the storage space with its implementation contract. As a consequence, all of the variables declared in the implementation contract are going to be stored in the proxy.

I illustrated this storage sharing in my workshop below:

2 Likes