Blockchain security isn't optional.
Protect your smart contracts and DeFi protocols with Three Sigma, a trusted security partner in blockchain audits, smart contract vulnerability assessments, and Web3 security.
Get a Quote Today
Introduction
Smart contracts on Ethereum are immutable by default, once deployed, their code cannot change. This immutability is a bedrock of trust, ensuring contract rules can’t be tampered with arbitrarily. However, immutability has a downside: if a critical bug is discovered or new features are needed, the original contract can’t be modified. The only naive solution is to deploy a new contract and migrate all users and data, a cumbersome process involving moving state, updating addresses, and convincing users to switch to the new deployment. In practice, this is error-prone, expensive, and user-disruptive.Upgradeable smart contracts solve this by introducing an indirection layer that separates a contract’s state from its logic. In an upgradeable architecture, users interact with a proxy contract that holds the persistent state and delegates calls to a logic contract (implementation) containing the code. To upgrade, a new implementation contract is deployed and the proxy is pointed to this new code, all while the proxy’s address and stored data remain unchanged. This way, developers can modify contract code while preserving the contract’s address, state, and balance.
Proxy Patterns
Upgradeable smart contracts in Ethereum are typically implemented using the proxy pattern. In this design, a proxy contract acts as a front-end that stores all the contract’s state and delegates any function calls to a separate implementation contract using the EVM DELEGATECALL instruction. DELEGATECALL allows the proxy to execute code from the implementation contract in the proxy’s context, meaning the implementation’s code can manipulate the proxy’s storage as if it were its own. The result is that users always interact with the proxy’s address, but the logic can be swapped by changing the implementation address the proxy points to.Several proxy patterns have emerged to facilitate upgrades, each with its own mechanism and use cases. The most widely used patterns in 2025 are Transparent proxies, UUPS proxies, Beacon proxies, and Diamond (multi-facet) proxies. All of these rely on a few low-level standards:
- Unstructured storage: To avoid collisions between proxy state and implementation state, specific storage slots (usually defined by EIP-1967) are reserved in the proxy for the implementation address and other proxy data. This ensures the proxy’s own variables (like the pointer to the implementation) don’t overwrite the implementation’s state variables. EIP-1967, is an Ethereum standard that fixes three deterministic storage slots: implementation, admin, and beacon, by taking keccak256("eip1967.proxy.<name>") − 1. These high, pseudo-random slots minimise collisions with implementation storage and with any other custom namespaces.
- Delegation logic: A base Proxy contract implementation typically provides a fallback() function that forwards any call to the current implementation using delegatecall. The success or failure and return data of the delegated call are then returned back to the caller, making the proxy largely transparent to end users.
With these basics in place, let’s outline each major proxy pattern:
Transparent Proxy
The Transparent Proxy pattern was one of the earliest popular upgradeable proxy designs (adopted by OpenZeppelin’s older SDK). A Transparent proxy includes built-in logic to differentiate between “admin” calls and “user” calls. The proxy has an admin address (often managed via a separate ProxyAdmin contract in OpenZeppelin’s implementation) that is authorized to upgrade the contract. When calls are made from the admin, the proxy will not forward those calls to the implementation. Instead, if the call data matches one of the admin functions (like an upgrade function), the proxy will execute its own admin routine. This prevents the admin from accidentally calling into the proxy’s own logic. Conversely, calls from non-admin accounts are always forwarded to the implementation logic. This split resolves potential function name collisions and ensures an admin cannot unintentionally trigger implementation functions (the “transparent” aspect).In practice, a Transparent proxy setup involves:
- An implementation contract deployed with the logic.
- A proxy contract (usually TransparentUpgradeableProxy) that holds the implementation address and an admin address. It implements functions like upgradeTo(newImpl) callable only by the admin, as well as the delegation fallback.
- A ProxyAdmin contract (in OpenZeppelin’s architecture) which is a single contract that actually holds the upgrade rights for one or multiple proxies. The deployer or governing entity controls the ProxyAdmin, and it in turn controls the proxy’s upgrades (the proxy’s admin is set to the ProxyAdmin’s address). This indirection lets a single multisig or DAO manage upgrades of many proxies via a single admin contract.
When using OpenZeppelin’s Upgrades Plugin in Transparent mode, a typical deployment flow is:
- Deploy the implementation contract (logic contract).
- Deploy the proxy contract, pointing it to the implementation, and call an initializer function on the proxy (instead of the constructor). The initializer sets up state (like initializing variables, setting ownership) in the proxy’s storage.
- Deploy a ProxyAdmin contract (if not already deployed). The proxy’s admin is set to this ProxyAdmin.
- To upgrade later, the project owner calls the ProxyAdmin’s upgrade(proxy, newImplementation) function, which in turn updates the proxy’s stored implementation address to the new one.
This pattern is called “transparent” because regular users cannot call the admin-only functions, any user call is forwarded to the implementation, and only the admin can trigger the special upgrade path. One drawback of Transparent proxies is a slight gas overhead: on every call, the proxy must check the caller against the admin address and potentially a function whitelist to decide how to route the call. Also, the extra ProxyAdmin layer means more contracts to deploy and manage.Key point: Transparent proxies have an explicit upgrade interface in the proxy itself and restrict who can use it. It’s a proven pattern with robust implementations, but as we’ll see, newer patterns aim to make proxies more lightweight.
UUPS Proxy
UUPS (Universal Upgradeable Proxy Standard) is a proxy pattern that shifts the upgrade logic out of the proxy and into the implementation contract. The name comes from ERC-1822, which first described this design. In a UUPS architecture, the proxy is extremely simple, essentially just a storage holder that delegates calls (often using a basic ERC1967Proxy contract), and it has no special code for upgrades or admin checks. Instead, the implementation contract itself includes a function (usually upgradeTo(address newImpl) or similar) that, when called by an authorized account, will directly write the new implementation address into the proxy’s storage slot (using the known EIP-1967 slot). In effect, the implementation contract is responsible for upgrading the proxy that points to it.OpenZeppelin’s UUPSUpgradeable base contract implements this by providing an internal _authorizeUpgrade() function (to be overridden with access control logic) and an upgradeTo function that performs the upgrade if authorized. Because the upgrade function lives in the implementation, only contracts that include the UUPS logic can trigger upgrades on the proxy. The proxy itself doesn’t check who is calling, it will delegate the call, and the logic contract will enforce permissions in _authorizeUpgrade.
Advantages
UUPS proxies remove the need for a ProxyAdmin contract and reduce deployment cost. The proxy is effectively just the bare minimum needed to delegate calls (hence cheaper in gas and simpler). OpenZeppelin notes that “TransparentUpgradeableProxy is more expensive to deploy than what is possible with UUPS proxies”, and they now recommend UUPS for most use cases due to its lightweight versatility. Another benefit is that the upgrade logic can be removed entirely in a final implementation if you want to lock in the code (making the contract effectively immutable thereafter). Developers can achieve this by upgrading to an implementation that doesn’t include the UUPS upgrade function, by renouncing the ownership if the access control system used allows or by otherwise disabling the upgrade function via a flag. This provides an “upgrade opt-out” path for a proxy if one ever wants to renounce upgradeability.
Security considerations
One must ensure that only authorized entities can call the upgradeTo function in UUPS. Typically, the implementation inherits UUPSUpgradeable and the developer overrides _authorizeUpgrade to allow only the owner (or a multisig/DAO) to call it. If this step is neglected, an attacker could directly call upgradeTo on the proxy (which gets delegated to the implementation) and thus replace the implementation with malicious code. It’s also crucial that the UUPS implementations are themselves upgrade-safe. In fact, early versions of OpenZeppelin’s UUPS had a known vulnerability where if an implementation contract was left uninitialized, an attacker could deploy their own implementation and perform an upgrade (we’ll discuss this in Part 2). OpenZeppelin v4.5+ added an ERC1822-compliance check to UUPS to prevent non-UUPS implementations from being set, avoiding accidentally “bricking” the proxy by upgrading to a contract missing the upgrade logic.In summary, UUPS proxies answer “where do we put the upgrade logic?” with: put it in the implementation. This pattern has become popular since it offers the same functional outcome as Transparent proxies with less overhead. The trade-off is that each implementation carries some extra code (the upgrade function) and careful design is needed to avoid mistakenly allowing upgrades from the wrong context (e.g., using a UUPS implementation with a Transparent proxy can be dangerous). With proper use, UUPS is considered a safe and efficient standard today.
Beacon Proxy
A Beacon proxy pattern involves an intermediate contract called a Beacon to manage the implementation address for multiple proxies. This was popularized by Dharma, an early-stage DeFi lending startup (2018-2020) and is useful when you have many proxy instances that should all use the same implementation (and be upgraded in unison). The setup is:
- A single UpgradeableBeacon contract holds an address of the current implementation and has an owner (admin) who can update that address.
- Multiple BeaconProxy contracts are deployed, each of which doesn’t store an implementation pointer itself, but instead knows the address of the Beacon contract. When a BeaconProxy receives a call, it queries the Beacon for the current implementation and then delegates to that implementation.
With this pattern, upgrading all proxies is as simple as calling upgradeBeacon(newImpl) once on the Beacon (by the admin), all BeaconProxy instances will then start delegating to the new implementation returned by the Beacon. This is efficient for scenarios like factory contracts that spawn many clone proxies for users: rather than upgrading hundreds of proxies one by one (as with Transparent or UUPS where each proxy is independent), a single transaction to the Beacon can update the logic for all of them at once.OpenZeppelin provides UpgradeableBeacon and BeaconProxy contracts. The Beacon has its own admin and upgrade function, while each BeaconProxy simply implements _implementation() to fetch from the Beacon’s address. Notably, the proxies themselves don’t have admin logic and are not individually upgradeable, the Beacon is the single point of upgrade control. One implication is that if a project uses a Beacon pattern, all instances trust the Beacon’s owner. For example, suppose a DeFi platform deploys 100 vault proxies using a Beacon; if the Beacon’s admin key is compromised, an attacker could upgrade the Beacon to malicious logic and thereby affect all 100 vaults at once. In contrast, with individual proxies (Transparent/UUPS), each could potentially have separate admins or at least the blast radius is one proxy at a time (unless they all share the same admin key anyway).In short, Beacon proxies are useful for upgradeable clone factories, providing an atomic multi-upgrade. This pattern sees use in upgradable ERC20 or ERC721 factory contracts, and other scenarios where one template is reused across many instances.
Diamond (EIP-2535)
The Diamond Standard (EIP-2535) takes proxy upgradeability to a more granular level. A “Diamond” is essentially a proxy that can have multiple implementation contracts (called facets) governing different functions. Instead of a single implementation address, the Diamond maintains a mapping of function selectors to facet addresses. There are standardized functions (named loupe functions) to add, replace, or remove functions in this mapping via an operation called diamondCut. In one transaction, you could swap out multiple functions to point to new facet contracts (hence upgrading multiple pieces of logic atomically). The Diamond’s fallback function then dispatches calls to the appropriate facet based on the function selector, similar to how a router works.
Key features of Diamonds:
- Modularity: You can organize contract logic into categories (facets) and upgrade them independently. This helps manage large contracts that might otherwise hit size limits. There’s effectively no size limit to a Diamond since you can always add more facets.
- Atomic upgrades: The diamondCut allows upgrading multiple facets in one go, ensuring that interdependent functions can be updated together consistently. The standard even allows an initializer function to be executed as part of a diamondCut to initialize any new state for facets being added.
- Introspection: EIP-2535 defines “loupe” functions that let anyone query which facets and functions a Diamond currently has. This aids transparency, users/UI can inspect the Diamond to see its composition at any time.
The Diamond pattern essentially generalizes the proxy idea: a Transparent or UUPS proxy is like a Diamond with one facet. A Diamond can have N facets, giving developers a lot of flexibility to compose and upgrade complex systems. It is especially useful for large systems (for example, the gaming app Aavegotchi and the DeFi protocol BarnBridge are known adopters of Diamonds).Drawbacks: Diamonds are arguably the most complex pattern to implement and reason about. The upgrade logic (diamondCut) itself must be carefully secured to avoid malicious replacements. Storage management is also more complex, typically Diamonds use a methodology where each facet uses its own storage “namespace” or struct to avoid slot conflicts (more on namespacing in section 4.3.3). Tools for Diamonds are less mainstream, though standards exist, and reference implementations are provided by the EIP author.
Conclusion
Part 1 provided an overview of what upgradeable smart contracts are and how they can be implemented using various proxy patterns. We learned about Transparent vs UUPS proxies (and why OpenZeppelin leans toward UUPS for new projects), as well as more advanced Beacon and Diamond patterns.Armed with this foundational knowledge, we are ready to delve into the darker side of upgradeable contracts. In Part 2, we will explore common vulnerabilities, pitfalls, and real-world hacks related to upgradeable contracts (from uninitialized proxies to malicious upgrades), learning from past failures to inform safer designs going forward.