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());
    }
}

Because this is a upgradeable contact we do not have a 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 {

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");

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

    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.

13 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.

3 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