Hello! Welcome to your eighth and final lesson in the "Advanced Solidity & Secure Development" module.
Introduction
In our last lesson, we established a strong theoretical foundation for smart contract upgradeability. We compared the Transparent and UUPS proxy patterns, concluding that UUPS is the modern, gas-efficient, and flexible standard for new projects, despite its unique risks.
Today, we transition from theory to practice. You will use your development skills to implement, deploy, and upgrade a smart contract using the UUPS pattern. We will leverage the industry-standard OpenZeppelin contracts and the Hardhat Upgrades plugin, which streamline this complex process and enforce critical safety checks.
Learning Outcome: By the end of this lesson, you will be able to implement an upgradeable contract using the UUPS proxy pattern.
We will build an upgradeable ERC-20 token, which serves as a perfect practical example and provides a smooth transition into our next module on DeFi primitives.
1. The UUPS Architecture with OpenZeppelin
Let's quickly recap the UUPS architecture before we start coding.
- Proxy Contract (
ERC1967Proxy): A minimal, generic contract that holds the state and the address of the current implementation. Users interact with this contract's address. - Implementation Contract (Your Logic): This contract contains the business logic. It is stateless itself but operates on the proxy's storage via
delegatecall. - Upgrade Logic: The logic to perform an upgrade (i.e., change the implementation address in the proxy) resides within the implementation contract.
This diagram shows the basic flow: a user calls the proxy, which delegates the call to the implementation contract.

To make this pattern safe and robust, OpenZeppelin's UUPSUpgradeable contract provides two crucial features:
- An
upgradeTo()function that handles the low-level storage update in the proxy. - A safety check mechanism that prevents you from upgrading to a new implementation that is not itself upgradeable, thus avoiding "bricking" your contract. This is done via a function called
proxiableUUID().

2. Implementing a UUPS Contract: A Practical Walkthrough
We will now walk through the process of creating and deploying a UUPS-compliant contract. The OpenZeppelin team has produced an excellent video that demonstrates this entire workflow using Hardhat. We will use it as our primary guide.
This video covers the theory we've discussed and then moves into a live-coding session that perfectly aligns with our learning outcome.
Deploying More Efficient Upgradeable Contracts
This video, 'Deploying More Efficient Upgradeable Contracts' by OpenZeppelin, is our main resource. It explains the UUPS pattern and then demonstrates its implementation with the OpenZeppelin Hardhat Upgrades plugin.
Please watch from 00:11:14 to 00:28:21. Part 1 (11:14 - 18:58): Focus on the explanation of the UUPS pattern's mechanics, its advantages in gas costs and flexibility, and the primary risk of accidentally removing the upgrade function. Part 2 (18:58 - 28:21): This is the core practical section. Pay close attention to the code modifications required to make a standard contract upgradeable. Specifically, note the changes from a constructor to an initialize function and the addition of UUPSUpgradeable and _authorizeUpgrade.
Summary of Key Implementation Steps
Based on the video, here is a checklist for converting a standard contract to a UUPS-upgradeable one:
-
Use Upgradeable Dependencies: Import contracts from
@openzeppelin/contracts-upgradeableinstead of@openzeppelin/contracts. For example, useERC20Upgradeable.solinstead ofERC20.sol. -
Replace
constructorwithinitialize:- The
constructoris replaced by a public function, conventionally namedinitialize. - This function must have the
initializermodifier (fromInitializable.sol) to ensure it can only be run once on the proxy. - Calls to parent contract constructors (e.g.,
ERC20("MyToken", "MTK")) are replaced with calls to their initializer functions (e.g.,__ERC20_init("MyToken", "MTK")).
- The
-
Inherit
UUPSUpgradeable: Your contract must inherit fromUUPSUpgradeableto get the upgrade logic. -
Implement Access Control for Upgrades:
- You must provide an access control mechanism for the upgrade function. A common choice is
OwnableUpgradeable. - You must override the
_authorizeUpgradefunction and protect it with your chosen access control (e.g., theonlyOwnermodifier).
- You must provide an access control mechanism for the upgrade function. A common choice is
Here is a complete example of a V1 contract incorporating these principles:
// contracts/MyTokenV1.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract MyTokenV1 is Initializable, ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address initialOwner) initializer public {
__ERC20_init("MyToken", "MTK");
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
_mint(initialOwner, 1000 * 10 ** decimals());
}
function _authorizeUpgrade(address newImplementation)
internal
onlyOwner
override
{}
}
3. Deploying and Upgrading with Hardhat
The @openzeppelin/hardhat-upgrades plugin makes deployment and upgrades straightforward.
- Deployment:
upgrades.deployProxy(ContractFactory, [initializer_args], { kind: 'uups' })- This deploys the implementation contract (
MyTokenV1). - It deploys the
ERC1967Proxycontract. - It calls the
initializefunction on the proxy with the provided arguments.
- This deploys the implementation contract (
- Upgrading:
upgrades.upgradeProxy(proxyAddress, NewContractFactory)- This deploys the new implementation contract (e.g.,
MyTokenV2). - It performs safety checks (like storage layout compatibility).
- It calls the
upgradeTo()function on the proxy to switch to the new implementation.
- This deploys the new implementation contract (e.g.,
Let's see this in action, along with a critical safety feature: storage layout validation.
Deploying More Efficient Upgradeable Contracts
Continuing with the same OpenZeppelin video, let's see how to perform an upgrade and what happens when we make a common but dangerous mistake.
Watch from 00:28:21 to 00:37:17. Focus on two key points: The upgradeProxy function call and how a simple V2 contract is created by inheriting from V1. The demonstration of a storage layout incompatibility error. Understand why changing or reordering state variables corrupts storage and how the plugin automatically prevents you from deploying such a broken upgrade.
4. Critical Security Considerations
While the tools provide many safeguards, secure implementation ultimately rests with you. Here are two non-negotiable security practices for UUPS proxies.
A. Storage Layout Invariance
As demonstrated in the video, the order and type of state variables must never change between upgrades.
- DO: Add new state variables at the end of your contract.
- DO NOT: Reorder, remove, or change the type of existing state variables.
If a variable becomes obsolete, you must leave it in place for storage compatibility, perhaps with a comment like // @deprecated - kept for storage layout compatibility.
UUPS Proxies: Tutorial (Solidity + JavaScript)
This point is so critical it's worth reading a clear, written explanation. This comment in an OpenZeppelin forum thread explains the issue perfectly.
In the comments section, find the discussion started by 'David_Hoang' about Contract1, Contract2, and Contract3. Read the explanation of why ct2data must be redeclared in Contract3 to prevent storage corruption. This reinforces the concept shown in the video.
B. The Uninitialized Implementation Vulnerability
The initialize function is public. This means anyone could call it directly on your implementation contract's address and become its owner. This could potentially allow them to execute a selfdestruct or other malicious code if your contract has such vulnerabilities.
The solution is to "lock" the implementation contract's initializer immediately upon deployment. You do this by calling _disableInitializers() in the implementation's constructor.
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
The constructor runs only once when the implementation contract is deployed. It has no effect on the proxy's state, making it the perfect place for this one-time setup task. This prevents anyone from ever calling initialize on the implementation contract.
UUPS: Universal Upgradeable Proxy Standard (ERC-1822)
This security measure is vital. The Rareskills article on UUPS provides an excellent explanation of this vulnerability and its solution.
Read the sections 'Vulnerabilities in UUPS' and 'A Checklist for Using UUPS'. Focus on understanding the 'Vulnerability with uninitialized contracts' and the solution using _disableInitializers() in the constructor.
Conclusion
Congratulations! You have now moved from understanding upgradeability in theory to implementing it in practice. You have the tools and knowledge to build flexible, long-lasting smart contract systems using the modern UUPS pattern.
Key Takeaways:
- Implementation: A UUPS contract inherits from
...Upgradeablecontracts, uses aninitializefunction with theinitializermodifier, inheritsUUPSUpgradeable, and implements_authorizeUpgradefor access control. - Tooling: The
@openzeppelin/hardhat-upgradesplugin is essential. It providesdeployProxyandupgradeProxyfunctions that handle the complex deployment and upgrade process while running crucial safety checks. - Security Best Practices:
- Never alter the storage layout of existing state variables between upgrades. Only append new variables.
- Always disable the initializer in the implementation contract's constructor by calling
_disableInitializers()to prevent malicious takeovers.
Next Steps:
This lesson concludes our module on advanced Solidity and security. You are now equipped with the patterns needed to write secure, flexible, and efficient smart contracts. In our next module, "Fungible Tokens & DeFi Primitives," we will apply these skills to build the foundational elements of decentralized finance, starting with a deep dive into creating a feature-rich ERC-20 token.
Can't find a good explanation? Sign up and we'll make it for you
Sign up