Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back.
In our last lesson, we explored how to manage a project's financial assets programmatically by executing a treasury transaction from a multi-sig wallet using the Safe{Core} SDK. This demonstrated how to secure your project's funds.
Today, we shift our focus from securing assets to managing the project's code. While smart contracts are famously immutable, this rigidity can be a double-edged sword. This lesson addresses the learning outcome: Explain the benefits and risks of upgradeable smart contracts. We'll explore the critical trade-off between the security of immutable code and the practical need to fix bugs and add features.
The Immutability Dilemma: Why Upgrade?
One of the core promises of blockchain is immutability—once code is deployed, it cannot be changed. This builds trust, as users know the rules of the contract are fixed. But what happens if a critical bug is discovered after launch? Or what if you want to add a highly requested feature to your protocol?
This is the central dilemma of smart contract development.
Upgrading your Smart Contracts | A Tutorial & Introduction
To start, let's watch the first minute of this excellent video from Patrick Collins. It clearly frames the problem: the conflict between the ideal of immutability and the practical need for upgrades.
Watch from the beginning until 00:58. Pay attention to how he distinguishes between changing a contract's state (like a user's balance) and changing its underlying logic.
As the video highlights, deploying a completely new contract and convincing your entire user base to migrate is often impractical, expensive, and fragments your protocol's ecosystem. This is why upgradeability is such a powerful, albeit complex, concept.
The primary benefits of making a contract upgradeable are:
- Bug Fixes: The ability to patch security vulnerabilities or correct errors without requiring a full-scale migration of state and users. This was a painful lesson learned from major exploits like The DAO hack.
- Feature Enhancements: You can add new functionality to your application over time, allowing your project to evolve and adapt to user needs and market trends.
- Data Consistency: Users interact with the same contract address and all their data (like token balances) remains intact during an upgrade, providing a seamless experience.
How Upgrades Work: The Proxy Pattern
Upgradeable contracts don't magically bypass immutability. Instead, they use a clever architectural pattern called the proxy pattern. This pattern separates the contract's state and logic into two distinct contracts:
- The Proxy Contract: This is the contract that users interact with. It holds all the state variables (the data) and has a permanent, unchanging address. Its main job is to forward all function calls to the implementation contract.
- The Implementation Contract (or Logic Contract): This contract contains the actual business logic for your application. It is stateless and can be replaced.
The magic that connects them is a low-level Solidity function called delegatecall.
To understand this crucial mechanism, let's turn back to the video.
Upgrading your Smart Contracts | A Tutorial & Introduction
This segment explains the core concept of delegatecall and how it enables the proxy pattern. Understanding this is key to grasping how upgrades are possible.
Watch from 06:12 to 08:30. Focus on the explanation of delegatecall: 'do contract B's logic in contract A'. Notice how the state is stored in the proxy, while the logic lives in the implementation.
In essence, when you call a function on the proxy, the proxy uses delegatecall to "borrow" the code from the implementation contract and execute it in its own context. This means the logic from the implementation contract modifies the storage of the proxy contract.
To perform an upgrade, you simply deploy a new implementation contract (e.g., MyTokenV2) and tell the proxy to point to this new address. Users continue to interact with the same proxy address, but their calls are now routed to the new logic.
The Risks of Upgradability
This flexibility comes with significant responsibility and a new set of potential vulnerabilities. Making a contract upgradeable introduces complexity and risks that must be carefully managed.
What is a Smart Contract Proxy Pattern?
For a comprehensive overview of the risks, we'll consult an article from Cyfrin, 'What is a Smart Contract Proxy Pattern?'. This will provide a solid textual foundation for the dangers involved.
Read the 'Security Considerations' section. It concisely lists the three main categories of risk: Collisions, Complexity, and Centralization.
Let's break down the most critical technical risks, using the "Security in Upgrades of Smart Contracts" video from OpenZeppelin for a deeper dive.
1. Centralization Risk
The most obvious risk is that an upgrade mechanism gives someone—an "admin"—the power to change the contract's rules at any time. A malicious or compromised admin could replace the logic with code that steals user funds.
Mitigation: This is where our previous lessons are crucial. The admin of an upgradeable contract should never be a single person's account (an EOA). It must be a decentralized governance mechanism, like the multi-sig wallet you set up, or a full DAO.
2. Storage Collisions
This is one of the most dangerous technical pitfalls. Because the implementation contract's logic operates on the proxy contract's storage, the storage layout of both must be compatible. A mismatch can lead to data corruption.
Security in Upgrades of Smart Contracts
The OpenZeppelin security workshop provides the best explanation of this risk. It visually demonstrates how misaligned storage slots can cause one variable to overwrite another.
Watch from 41:31 to 47:30. Pay close attention to the concept of 'storage slots' and the diagram showing how the initialized variable in the implementation could collide with the implementation address in the proxy. Also, note the strict rules for adding new variables in subsequent versions (you can only append).
As the video explains, you must be extremely careful when upgrading:
- Never change the order of existing state variables.
- Never change the type of an existing state variable.
- Never remove a state variable.
- You can only append new state variables to the end of your contract.
3. Initialization Vulnerabilities
Since proxy contracts are involved, you cannot use a constructor to initialize your state, because the constructor logic would run on the implementation contract, not the proxy. Instead, upgradeable contracts use a public initialize function.
This creates new risks:
- Forgetting to Initialize: If you deploy your proxy and forget to call
initialize, an attacker could call it for you and, for example, set themselves as the owner. - Re-Initialization: If the
initializefunction isn't protected, an attacker could call it again later, potentially resetting the contract's ownership or state.
Security in Upgrades of Smart Contracts
Let's see how these initialization issues can manifest. This next clip from the OpenZeppelin workshop covers these exact scenarios.
Watch from 21:51 to 27:30. Focus on why a simple initialize function isn't enough and how it can be re-initialized. This motivates the need for special modifiers like initializer provided by libraries like OpenZeppelin.
4. Bricked Upgrades
This is a particularly nasty problem associated with the UUPS (Universal Upgradeable Proxy Standard) pattern, which we will implement in the next lesson. In this pattern, the upgrade logic itself resides in the implementation contract. If you accidentally deploy a new implementation version (V2) that forgets to include the upgrade logic, your contract becomes "bricked"—it can never be upgraded again.

Test your understanding!
Imagine you have an upgradeable contract MyTokenV1 with a state variable uint256 public totalSupply;. For MyTokenV2, you want to add a new variable to track the owner's address: address public owner;.
You modify the V2 contract to look like this:
contract MyTokenV2 {
address public owner;
uint256 public totalSupply;
// ... rest of the logic
}
What will happen when you upgrade the proxy to this new implementation?
Show answer
This will cause a critical storage collision. The first storage slot (slot 0) in the proxy holds the totalSupply value. When the V2 logic executes, it will treat slot 0 as the storage for the owner address. This will corrupt the totalSupply data, interpreting a large integer as an address, and any attempt to set the owner will overwrite the totalSupply. The correct approach is to append the new variable: uint256 public totalSupply; address public owner;.
Conclusion
Upgradeable smart contracts represent a powerful solution to the challenge of evolving decentralized applications. They provide the flexibility to fix bugs and add features, which is crucial for the long-term viability of any complex project.
However, this flexibility is not free. It introduces a new layer of complexity and significant risks that must be managed with extreme care.
Key Takeaways:
- Benefits: Upgradability allows for bug fixes and feature additions without costly and disruptive user migrations.
- Mechanism: It works via the proxy pattern, where a stateful proxy uses
delegatecallto execute logic from a replaceable implementation contract. - Major Risks:
- Centralization: The upgrade admin must be secured, ideally with a multi-sig or DAO.
- Storage Collisions: The storage layout between versions must be strictly preserved.
- Initialization Errors: Standard
constructorsdon't work, andinitializefunctions must be carefully protected.
- Mitigation: The best way to manage these risks is to use battle-tested libraries like OpenZeppelin Upgrades, which automate many safety checks, and to follow established security practices.
Preview of the Next Lesson:
Now that we've covered the theory, it's time to get practical. In our next lesson, we will implement an upgradeable contract using the UUPS proxy pattern with OpenZeppelin. You will learn how to configure Hardhat and use the OpenZeppelin Upgrades plugin to deploy and manage an upgradeable version of a smart contract, putting today's concepts directly into practice.