Smart contract upgradeability is achieved by splitting state and logic across two contracts, so that a proxy holds the storage while a swappable implementation holds the code.

Smart contract upgradeability is achieved by splitting state and logic across two contracts, so that a proxy holds the storage while a swappable implementation holds the code. Many Ethereum protocol upgrades over the last decade have had unintended effects such as storing data in colliding storage locations, "bricking" a particular type of proxy, or even handing attacker-controlled addresses "upgrade authority" in some form or another. The notion of "upgradeability" in smart contracts sounds very positive, but translates very poorly to actual implementation details. Solidity, the programming language for smart contracts on Ethereum, provides a set of proxy patterns, all of which have tradeoffs in terms of gas cost, the size and complexity of the access control surface, and the danger of slot corruption.
This post details the storage collisions that can occur at the EVM level, the difference between an initializer and a constructor and how this affects storage, and finally the problems of previous migration sequences that have resulted in the locking of funds or permanent destruction of contract logic. Although proxy patterns in Solidity have reached a level of maturity that makes them safe to use, it is important for developers to understand the specific failure modes for each of the patterns in order to use them safely and to write safe upgrade logic.
Choosing whether to make a protocol upgradeable shapes trust, governance, and long-term security from day one.
Immutability is a feature. Users who interact with a fixed contract know its behavior cannot be silently altered, removing a critical trust assumption. Introducing upgradeability trades that certainty for flexibility, a genuine immutability trade-off that must be explicitly managed through documentation and audits.
Upgradeable smart contracts suit protocols under active development or operating in regulatory-sensitive environments where post-launch bug fixes are essential. Narrow, well-audited primitives, ERC-20 tokens, simple vaults, rarely need it; immutable deployment is the safer choice there.
Upgrade authority, whether held by a single admin key or a multisig, is often the largest centralization risk in protocol governance. Disclosing this in audits and docs directly affects how investors assess decentralization and how users gauge counterparty risk.
There are many patterns, but most involve the following basic architecture: a proxy holds state and a backend that implements logic, connected via delegatecall. Understanding proxy patterns solidity developers commonly reach for is essential, as these are the root causes of many upgrade issues people hit with production smart contracts.
Transparent proxies, implemented for example with the OpenZeppelin proxy library, split up the routing of calls based on the caller. Admin calls are routed to the proxy, user calls are forwarded to the implementation. The implementation must take care not to overwrite function selectors of the proxy in storage, and in every user call a storage read is incurred with measurable gas cost in big scaled applications.
EIP-1967 reserves certain storage slots for the implementation contract and the admin address of a proxy. Using this as a basis, the UUPS proxy implementation then moves the upgrade logic to the implementation contract. The key UUPS vs transparent proxy distinction is that this shift introduces a severe tradeoff: if one deploys an implementation contract that is missing the function to perform an upgrade (i.e. the upgrade function is not implemented) then the corresponding proxy is 'bricked' and there is no way to recover from this.
Beacon proxy is preferred for a large number of identical contracts deployed by a factory, such as user vaults. A single beacon update is then propagated to all instances in atomic fashion, avoiding the upgrade coordination overhead of individually updating to proxy a contract that has been upgraded. Teams that need this topology implemented and reviewed end to end usually scope it as part of smart contract development services.
The proxy code and the implementation code live in the same address space delegatecall storage. So one small misplaced variable can very silently corrupt some parts of the code that are running.
In Solidity, the storage slots for state variables are assigned sequentially from slot 0 onwards. In the case of a proxy/implementation, the slot 0 of the proxy (i.e. the outer contract) is also the slot 0 of the implementation (i.e. the inner contract). Thus, any discrepancy between the storage layout in the two contracts will write over the top of the contract's live storage without any error messages or reversion.

When a new variable is added to a parent contract in the middle of an upgrade, all subsequent storage slots of the child contract will end up being corrupted with user's balances or critical addresses. To safely add variables in base contracts, OpenZeppelin uses storage gap pattern reserving a block of slots (e.g. uint256[50] private __gap) that are then skipped over in subsequent adds.
ERC-7201 namespaced storage structures can use keccak256 to derive a deterministic slot for each storage structure member. These incompatibilities can be found by OpenZeppelin's Hardhat and Foundry Upgrades plugins as well as by Slither's layout checker before deployment. Layout validation is also one of the first things covered in a blockchain security audit of a proxy-based system.
This means the constructor executes on the implementation contract in deployment time, not on the proxy. Therefore the state of the proxy's storage context is never affected by the constructor.
Note that a constructor proxy relationship is broken because delegatecall executes within the storage of the proxy, which the implementation's constructor never writes to. Hence all variables declared within the constructor of an implementation will only ever exist in the address space of the implementation, and thus will be useless.
Unlike a constructor, an initialize function must be marked as Initializable by OpenZeppelin and called in a manner that prevents re-initialization, a proven attack vector in contract execution.
The OpenZeppelin implementation of a reinitializer (function reinitializer(uint64 version)) is used to safely allow for multiple setup of upgradeable contracts in a multi-step migration. Any initializer function on the bare implementation contract should also be called as part of the initial deployment of the contract to prevent an attack to initialize the (currently uninitialized) implementation contract first.
Pushing an upgrade to a live protocol introduces failure modes that extend well beyond compilation errors or logic bugs.
State migration is the highest-risk phase of any upgrade. If new storage variables require computed initial values, such as restructuring a mapping, that logic must execute atomically and be tested against a mainnet fork before deployment. A well-defined smart contract migration strategy becomes critical when migration scripts are tested only on local networks, where edge cases in live protocol state don't surface.
Upgrade transaction front-running is a concrete threat: the pending transaction is visible in the mempool, giving sophisticated actors a window to exploit the gap between old and new implementations. OpenZeppelin's TimelockController, configured with a 48 to 72 hour delay, closes this window while also giving users time to exit before a contentious change takes effect.
An upgrade rollback restores the proxy pointer but not corrupted storage, making true reversal nearly impossible. Pre-upgrade state snapshots, simulation on a forked mainnet, multisig ceremony execution, and post-upgrade invariant checks form the minimum viable runbook for any protocol serious about operational safety.
Who controls the upgrade process of a protocol is who controls the protocol. Poor key management of security measures compromises them all.
In terms of risk, a single EOA managing upgrade access control is a single point of failure in the form of a compromised private key fully taking over the entire protocol. A 3-of-5 Gnosis Safe multisig is the bare minimum a mainnet protocol should utilize for user held funds. On-chain governance with Governor Bravo adds further decentralization but introduces the risk of the multisig for upgrades. This can include low quorum and highly concentrated token holders ability to pass malicious proposals.
A timelock controller can also be used to keep upgrades pending for a while (typically 24 to 72 hours) to allow users to withdraw their funds before a possibly contentious upgrade is applied. It is even possible to make such a timelock controller contract-tamper-resistant using OpenZeppelin's TimelockController smart contract.
Note that a desired end state of on-chain governance does not have to be the launch point for a protocol. A protocol can launch with multisig, then add a timelock at certain TVL milestones before passing off governance to a DAO as the token circulation broadens, following a pattern of progressive decentralization.
Run Foundry upgrade tests or Hardhat forked against live state to find storage collisions and broken initializers that were not found by your clean-state unit tests. Use invariant fuzz testing post-upgrade to test core protocol invariants such as total supply == sum of all balances in the system.
Both the Hardhat Upgrades plugin and the forge-upgrades command from Foundry run automated storage layout checks on upgrade between old and new code and report any conflicts between the two on storage in slots.
Auditing an upgradeable contract includes identification of unprotected initializers, checks for missing UUPS authorization, identification of differences in storage layout, and audit of the length of time of the lock time to ensure it is appropriate for the contract. See the smart contract security framework for more on this.
Three diagnostic questions to decide if smart contract upgradeability is ready to be deployed.
However, being unable to answer the following questions means that your protocol is not ready for mainnet yet: Who has the keys to update? What is the timelock delay? How is storage layout validated? Shipping upgradeable contracts to run on mainnet without having these answers written down expands the attack surface to include the upgrade mechanism itself.
Whether to opt for a project that is immutable or upgradeable generally depends on several scope conditions. Immutability on projects like ERC-20 tokens is generally the better choice as a new version can be deployed and users migrated via the user interface. The decision on immutability or upgradeability should be included in your security model and communicated with auditors and investors accordingly.
The trade-offs for gas, governance, and failure between Transparent, UUPS, and Beacon proxy upgrade patterns all depend on your contract's storage layout, initializer behavior, and upgrade authority. Storage slot collisions and uninitialized proxy contracts are among the most damaging failure modes, so proper testing of upgradeable contracts with a robust testing setup including forking of test environments and pre- and post-upgrade invariant checking is crucial. Immutability and upgradeability are not opposites, and the OpenZeppelin Upgrades plugin surfaces these kinds of storage and initialization errors in existing codebases before they reach production. Teams scoping an upgrade architecture alongside the rest of their protocol stack can review the available Web3 development capabilities before locking the design in.
The greatest dangers lie in the storage layout being redefined so that old variables get overridden by new ones; initializers not being correctly set up for a contract so it is not properly initialized or configured after an upgrade; the authority to upgrade a contract being a single point of failure and thus prone to being compromised by an attacker; and upgrades being done too quickly and not getting adequate testing on a forked mainnet before being deployed in production. Understanding upgradeable smart contract risks in full before committing to a proxy pattern is essential, as careless implementation can result in large amounts of funds being irretrievably lost.
A storage layout collision can occur if a new contract for an implementation of a contract stores its state variables in a different order, or even inserts new variables in between currently stored variables. Since the proxy contract is only there to call the functions of the implementation contract while preserving the storage of the proxy contract, storage collisions will silently corrupt the state of the live contract. OpenZeppelin uses the storage gap pattern for storage, and ERC-7201 namespaced storage as the two primary methods to mitigate this class of issue. Run your typical audits (e.g. slither) and a custom storage diff script on your typical upgrade deploy scripts.
The case for UUPS (EIP-1822) upgrades for new projects is to keep the upgrade logic within the implementation contract. The overhead for deploying a proxy in these cases is more than offset by not embedding a giant proxy contract. On the other hand, Transparent proxies have the advantage that it is harder to accidentally remove the upgrade mechanism, which can be a big deal for forward facing contracts. For operational safety aware protocols, UUPS may not be the best approach for them, but for teams with good test coverage and smaller budgets, UUPS can be a good choice.
When immutability is a key trust guarantee for your users, then it is better to deploy non-upgradeable contracts such as for decentralized custody, settlement layer contracts or even token contracts where users trust that there will never be any changes to the rules for that contract. Allowing upgradeability at some point in time for such protocols only results in more surface area for potential attacks from a governance perspective once a security bug is found. Such contracts only need a full migration to a fixed version in case of bugs found after deployment. Founders should treat upgradeability as a time-limited scaffold to deploy a protocol and then plan a fixed, immutable version as the protocol matures.



