
Smart contracts are supposed to be immutable. Once deployed, their code is expected to remain unchanged. That immutability is one of blockchain’s strongest security properties, but it creates an obvious problem for production protocols.
This is where smart contract upgradeability comes in. Upgradeability allows developers to change contract logic while preserving the same user-facing contract address and, in most designs, the existing state.
But there is a catch:
An upgrade mechanism is effectively a privileged path for changing what your smart contract can do after deployment.
That means the upgrade system itself becomes part of the protocol’s attack surface. And this is where many teams get it wrong.
Most upgradeable Ethereum contracts use some variation of the proxy pattern. Instead of putting everything into one contract, the architecture separates:
When a user calls the proxy, the proxy forwards execution to the implementation using EVM’s delegatecall.



The important detail is that delegatecall executes the implementation’s code in the proxy’s storage context. So if the implementation contains:
balances[msg.sender] += amount;
The storage being modified belongs to the proxy. An upgrade, therefore, does not replace the proxy itself. Instead, the proxy is pointed toward a different implementation contract.
This is why upgradeability is powerful and dangerous.
Ethereum’s documentation describes this model as separating storage from logic and changing the implementation address to modify the behavior of the existing contract.
The most obvious risk is also one of the most underestimated. If an attacker gains control of the upgrade authority, they may not need to exploit the protocol’s business logic at all. They can simply deploy malicious implementation code and upgrade the proxy.
For example:
Normal implementation
↓
User deposits 100 ETH
↓
Proxy
↓
Secure logic
After a compromised upgrade key:
Malicious implementation
↓
User deposits 100 ETH
↓
Proxy
↓
Attacker-controlled logic
The contract address hasn’t changed. The user’s interaction hasn’t changed. The frontend may even look identical. But the code executing behind that address has changed.
Do not treat the upgrade key like an ordinary deployment wallet. Use stronger controls such as:
OpenZeppelin’s tooling supports different upgrade patterns and explicit ownership mechanisms, but the security of the upgrade authority remains a fundamental design responsibility.
The key principle: protect the upgrade path with at least the same seriousness as the funds themselves.
This is one of the most technical — and most frequently underestimated — risks. Upgradeable contracts preserve state across implementations. That means the storage layout of version 1 and version 2 must remain compatible. Consider:
// Version 1
address owner;
mapping(address => uint256) balances;
uint256 totalSupply;
Now imagine version 2 changes the order:
// Version 2
uint256 totalSupply;
address owner;
mapping(address => uint256) balances;
The Solidity code may compile perfectly. But storage slots don’t magically understand your intentions. The EVM simply sees storage positions.
Version 1 might interpret:
Slot 0 → owner
Slot 1 → balances
Slot 2 → totalSupply
while version 2 interprets those same locations differently. The result can be corrupted state, broken permissions, incorrect balances, or much worse.
OpenZeppelin specifically warns that storage collisions can occur between implementation versions when variables are reordered or incompatible variables are introduced.
For upgradeable contracts:
Do not reorder existing storage variables.
Generally:
This is one reason upgrade validation tooling is so valuable.
A normal Solidity contract uses a constructor:
constructor(address admin)
{
owner = admin;
}
But constructors run when the implementation contract itself is deployed. With proxies, users interact with the proxy, so initialization needs to happen through the proxy’s execution context. Upgradeable contracts therefore commonly use an initializer:
function initialize(address admin) external initializer
{
owner = admin;
}
The danger is simple:
If initialization is not properly protected, an attacker may be able to initialize the contract with themselves as the owner or administrator. That turns a deployment mistake into a complete privilege takeover. Developers should therefore:
UUPS proxies are attractive because the upgrade mechanism lives in the implementation rather than requiring a heavier proxy-side upgrade mechanism. But that creates an important security consideration.
The implementation contains the function responsible for authorizing upgrades. In simplified form:
function upgradeToAndCall
(
address newImplementation,
bytes calldata data
) external;
The critical question becomes:
OpenZeppelin’s UUPS implementation requires developers to override _authorizeUpgrade() with an appropriate access-control mechanism. A poorly implemented authorization check can effectively expose the entire protocol to arbitrary upgrades.
Even more subtly, an upgrade can modify the future upgrade mechanism itself. That means developers must audit not only:
“Can someone upgrade the contract?”
but also:
“What upgrade powers will the new implementation have?”
This distinction is easy to miss.
Smart contract functions are represented by 4-byte function selectors. That sounds like plenty of space. It isn’t. Different function signatures can theoretically produce the same selector.
In proxy architectures, this creates another layer of complexity because the proxy itself may expose administrative functions while the implementation exposes application functions.
If selectors collide, the proxy may intercept a call that developers expected to reach the implementation. Ethereum’s EIP-1967 specifically discusses this risk and standardizes proxy storage locations partly to avoid exposing proxy-management functions that could clash with implementation functions.
Transparent proxies address this through caller-dependent routing:
This is why proxy architecture isn’t simply a deployment detail. The routing mechanism itself can affect application behavior.
Beacon proxies are useful when many proxy instances share the same implementation. Instead of upgrading each proxy individually:
Proxy A ─┐
Proxy B ─┼──> Beacon ──> Implementation
Proxy C ─┘
Changing the beacon’s implementation can upgrade all connected proxies. That is operationally convenient. But it also creates a larger blast radius. A compromised beacon can potentially affect every contract relying on it.
OpenZeppelin describes beacon proxies as a mechanism where multiple proxies can be upgraded by changing the implementation referenced by their shared beacon. So, before using a beacon architecture, founders should ask:
“If this upgrade authority is compromised, how many contracts can an attacker affect?”
That answer should influence governance, monitoring, and emergency controls.
Not every dangerous upgrade contains an obvious coding vulnerability. Imagine an upgrade that changes:
fee = 0.3%;
to:
fee = 30%;
The contract may compile. Storage may be compatible. All tests may pass. Access control may be correct. Yet the protocol’s economics have fundamentally changed. This is why upgrade security cannot stop at:
It must also ask:
This is where upgrade reviews need to combine code security with economic security.
A common mistake is assuming:
“The contract is already audited, so upgrades are safe.”
That assumption is dangerous. The original implementation may have been audited. The new implementation is new code. Its interaction with existing storage, governance, integrations, and user positions is also new. A serious upgrade process should therefore include:
OpenZeppelin provides upgrade plugins specifically to validate upgrade safety and compatibility before an implementation is deployed.
Upgradeability solves a real engineering problem: how do you evolve an immutable system? But it introduces another problem:
Who gets to decide what the system becomes?
That question is more important than whether the protocol uses Transparent, UUPS, Beacon, or another upgrade pattern. A secure upgrade architecture should establish four clear boundaries:
Upgrade Governance
↓
┌─────────────────┐
│Upgrade Authority│
└───────┬─────────┘
↓
New Implementation
↓
Storage Compatibility
↓
User Funds
Every layer needs independent controls. The upgrade authority must be protected. The implementation must be validated. Storage compatibility must be enforced. And the resulting behavior must be monitored after deployment.
Smart contract upgradeability is not simply a way to “make immutable contracts editable.” It creates a controlled code-replacement system around an otherwise immutable protocol. That system introduces risks around:
For crypto founders, the right question isn’t:
“Should our smart contracts be upgradeable?”
It is:
“If our contracts are upgradeable, can we prove that no single compromised key, implementation, or governance action can silently take control of user funds?”
That is the standard worth designing for. And as protocols move billions of dollars on-chain, upgradeability should be treated as a security-critical subsystem — not a deployment convenience.
Smart Contract Upgradeability: Security Risks Developers Often Miss was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.