How to create an UUPS Proxy
What is UUPS Proxy?
UUPS stands for Universal Upgradeable Proxy Standard and was first documented in EIP1822. In this proxy pattern the responsibility of upgrading the implementation contract address is with the implementation contract itself. In contrast, in the transparent proxy pattern the responsibility of upgrading the implementation contract address is that of the proxy contract and the proxy admin contract. For a more detailed explanation of the transparent proxy pattern check out this article.
When we try to upgrade a smart contract deployed with the UUPS proxy pattern, the proxy contract makes a call to the implementation contract. Which checks if the user upgrading has permission to upgrade the implementation and then goes ahead and changes the implementation contract address to a new one.
UUPS proxy pattern is recommended over transparent proxy pattern because it saves gas during initial deployment by removing the need for proxy admin contract, which is required in transparent proxy pattern. There is one caveat when using UUPS proxy pattern that needs to be kept in mind. If you upgrade your proxy with a non UUPS compatible implementation you can never upgrade it afterwards. Such a mistake can be fatal as you have lost the upgradeability which was our backdoor to fixing such mistakes.
Let’s dive in and deploy a smart contract with the UUPS proxy pattern.
This Github repository has the code for this article feel free to save it for future use. You can clone it if you want to follow along without setting up from scratch.
Project Set Up
We will be using Hardhat for our Ethereum development workflow. I am using WSL2 in a windows machine for this walkthrough. You can adjust the steps slightly in case you are using a different set up. Let’s get started!
Run the following commands to create a directory and initialize it as a node project.
mkdir hardhat-upgrades && cd hardhat-upgradesnpm init -y
Install hardhat in this directory.
npm i --save-dev hardhat
Initialize a hardhat type script project in the directory. Run the below command and choose “Create a TypeScript project” option. Go with the default parameters for rest of the prompts.
npx hardhat
Install the Open zeppelin hardhat upgrades plugin which we’re going to use to easily deploy our proxy.
npm i --save-dev @openzeppelin/hardhat-upgrades
Also install the upgradeable contracts from Open zeppelin which is the upgradeable counterpart of Open zeppelin contracts.
npm i --save-dev @openzeppelin/contracts-upgradeable
Copy and paste the following into the hardhat.config.ts file. This will do the basic hardhat set up we require for this project.
import { HardhatUserConfig } from "hardhat/config";import "@nomicfoundation/hardhat-toolbox";import "@openzeppelin/hardhat-upgrades";import "@typechain/hardhat";const config: HardhatUserConfig = {solidity: {version: "0.8.9",settings: {optimizer: {enabled: true,runs: 200,},},},defaultNetwork: "localhost",};export default config;
Implementation contract to deploy with UUPS Proxy
Inside contracts folder create a folder for the implementation contract for which we’re going to deploy a UUPS proxy.
mkdir contracts/uups
In the contracts directory create a file named VersionAware.sol and copy paste the following code in it.
// SPDX-License-Identifier: Unlicensepragma solidity ^0.8.0;abstract contract VersionAware {string public versionAwareContractName;function getContractNameWithVersion()externalpurevirtualreturns (string memory);}
Here we create a basic skeleton of our implementation contracts so that we can easily see the version upgrades visibly after deployment and upgrade.
In the contracts/uups directory create two files named UupsProxyPatternV1.sol and UupsProxyPatternV2.sol and copy paste the following code into it.
UupsProxyPatternV1.sol
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {VersionAware} from "../VersionAware.sol";contract UupsProxyPatternV1 is
UUPSUpgradeable,
OwnableUpgradeable,
VersionAware{constructor() {
_disableInitializers();
}function initialize() external initializer {
versionAwareContractName = "UUPS Proxy Pattern: V1";
///@dev as there is no constructor, we need to initialise the OwnableUpgradeable explicitly
__Ownable_init();
}///@dev required by the OZ UUPS module
function _authorizeUpgrade(address) internal override onlyOwner {}function getContractNameWithVersion()
public
pure
override
returns (string memory){
return "UUPS Proxy Pattern: V1";
}
}
UupsProxyPatternV2.sol
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {VersionAware} from "../VersionAware.sol";contract UupsProxyPatternV2 is
UUPSUpgradeable,
OwnableUpgradeable,
VersionAware{constructor() {
_disableInitializers();
}function initialize() external reinitializer(2) {
versionAwareContractName = "UUPS Proxy Pattern: V2";
///@dev as there is no constructor, we need to initialise the OwnableUpgradeable explicitly
__Ownable_init();
}///@dev required by the OZ UUPS module
function _authorizeUpgrade(address) internal override onlyOwner {}function getContractNameWithVersion()
public
pure
override
returns (string memory){
return "UUPS Proxy Pattern: V2";
}
}
Note that in both the contracts we inherit from UUPSUpgradeable.sol which makes out contract UUPS upgrade compatible. Inheriting from this contract requires us to implement the _authorizeUpgrade function that is used to decide whether the caller is allowed to upgrade or not.
Here I attempt to explain a few advanced details of the the above contracts. Consider skipping it now and coming back later if you have difficulty understanding this on first read. Typical upgradeable contracts should not have a constructor because the constructor of the implementation contract can never be run in the context of the proxy contract. We have added a constructor here which is safe as it does not set to any of the storage variables. Leaving a contract without initializing it can pose a security threat. Calling the _disableInitializers method in the constructor makes the implementation contract not initializable which is much safer than leaving the implementation without a constructor and not initialized. Note that I have used initializer modifier in the initialize method in V1. This modifier makes sure that this initialize method is called only once, like solidity ensures for constructors. Also note that in V2 the modifier of the initialize method is reinitializer(2). Here 2 represents the version of the implementation contract. The reinitializer modifier has to be used instead of initializer because the proxy contract has already been initialized once in V1. There are more things and details to know about Initializer.sol contract which all upgradeable smart contracts should inherit. I will write more detailed article on it in future.
Now, run the following command to compile the smart contracts.
npx hardhat compile
UUPS Proxy Deployment and Upgrade
First we will deploy version 1 with UUPS proxy and then upgrade it to version 2. We will use a hardhat script to do this. Create a script named uups.js in scripts folder and copy paste the following code in it.
const { ethers, upgrades } = require("hardhat");async function main() {const UupsProxyPatternV1 = await ethers.getContractFactory("UupsProxyPatternV1");const uupsProxyPatternV1 = await upgrades.deployProxy(UupsProxyPatternV1, [], {kind: 'uups', unsafeAllow: ['constructor']});await uupsProxyPatternV1.deployed();console.log(`UUPS Proxy Pattern V1 is deployed to proxy address: ${uupsProxyPatternV1.address}`);let versionAwareContractName = await uupsProxyPatternV1.getContractNameWithVersion();console.log(`UUPS Pattern and Version: ${versionAwareContractName}`);const UupsProxyPatternV2 = await ethers.getContractFactory("UupsProxyPatternV2");const upgraded = await upgrades.upgradeProxy(uupsProxyPatternV1.address, UupsProxyPatternV2, {kind: 'uups', unsafeAllow: ['constructor'], call: 'initialize'});console.log(`UUPS Proxy Pattern V2 is upgraded in proxy address: ${upgraded.address}`);versionAwareContractName = await upgraded.getContractNameWithVersion();console.log(`UUPS Pattern and Version: ${versionAwareContractName}`);}// We recommend this pattern to be able to use async/await everywhere and properly handle errors.main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
We first deploy version 1 of the smart contract with proxy. The Open zeppelin upgrades deployProxy method takes care of it for us. Once version 1 is deployed with proxy we call the getContractNameWithVersion function on the proxy. This method will return a string according to what we made this method return in version 1 of the contract. Then we proceed to upgrade this contract with the upgradeProxy method. After the upgrade is over we call the getContractNameWithVersion function again to see the change in the string returned. Let’s run the script and see the results. Use the following command to run the script.
npx hardhat run scripts/uups.js
You should see the following printed on your console.
Warning: Potentially unsafe deployment of UupsProxyPatternV1You are using the `unsafeAllow.constructor` flag.Warning: A proxy admin was previously deployed on this networkThis is not natively used with the current kind of proxy ('uups').
Changes to the admin will have no effect on this new proxy.UUPS Proxy Pattern V1 is deployed to proxy address: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
UUPS Pattern and Version: UUPS Proxy Pattern: V1
Warning: Potentially unsafe deployment of UupsProxyPatternV2You are using the `unsafeAllow.constructor` flag.UUPS Proxy Pattern V2 is upgraded in proxy address: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
UUPS Pattern and Version: UUPS Proxy Pattern: V2
Yay! the results are as expected. The implementation contract is successfully upgraded to version 2. This is how you deploy and upgrade future versions of your contract with the UUPS proxy pattern however complicated your smart contract gets.
Thanks for reading.
Who am I?
I am a full stack blockchain developer, passionate about building a decentralized and potentially more inclusive future. Have a blockchain development need?
Get in touch: 📧 hariharan@alumni.iitm.ac.in
New to trading? Try crypto trading bots or copy trading