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.
- We will write our contract source code using OpenZeppelin Upgradeable Contracts.
- 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.