Skip to content

UUPS vs beacon vs transparent proxies: choosing an upgrade pattern

How UUPS, beacon and transparent proxies work, what each costs, where they go wrong, and how to choose an upgrade pattern for an Ethereum protocol.

· 4 min read

  • Upgradeable contracts
  • Solidity
  • Architecture

Most production protocols on Ethereum are upgradeable in some form. Upgradeability lets you fix bugs and add features, but it is also a large part of your attack surface and your users’ trust model. The pattern you choose affects gas, deployment, governance and the mistakes you can make.

This article compares the three patterns you’ll meet most often, explains where each goes wrong, and suggests how to choose.

How proxies work

All three patterns use the same trick. Users interact with a proxy contract that holds the storage and balance. The proxy forwards every call with delegatecall to an implementation contract that holds the code. delegatecall runs the implementation’s code against the proxy’s storage. Upgrading means pointing the proxy at a new implementation.

The patterns differ in two questions: where does the upgrade logic live, and where is the implementation address stored.

Transparent proxy

The proxy itself contains the upgrade function, guarded by an admin. To avoid clashes between the proxy’s admin functions and the implementation’s functions, calls from the admin are handled by the proxy and calls from everyone else are forwarded. In OpenZeppelin’s current version the admin is a dedicated ProxyAdmin contract.

  • Pros: upgrade logic can’t be removed by a bad implementation, and the pattern is well understood.
  • Cons: every call pays for the admin check, and each proxy has its own ProxyAdmin, which adds deployment cost and operational overhead.

UUPS (ERC-1822)

The upgrade function lives in the implementation, typically by inheriting OpenZeppelin’s UUPSUpgradeable and overriding _authorizeUpgrade:

contract Vault is UUPSUpgradeable, AccessManagedUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize(address authority) external initializer {
        __AccessManaged_init(authority);
    }

    function _authorizeUpgrade(address) internal override restricted {}
}
  • Pros: the proxy is minimal, so calls are cheaper, and upgrade permissions can use the same access control as the rest of the protocol.
  • Cons: the upgrade ability depends on every implementation. Deploy one without a working upgradeToAndCall, or with _authorizeUpgrade left open, and the proxy is bricked or anyone can upgrade it. OpenZeppelin’s implementation checks the new implementation’s proxiableUUID to catch the first mistake, but not the second.

Beacon proxy

Many proxies point at a single beacon contract, and the beacon stores the implementation address. Each call, the proxy asks the beacon which implementation to use. Upgrading the beacon upgrades every proxy at once.

  • Pros: ideal when a factory deploys many instances of the same contract, such as one vault per asset or one market per borrower. One transaction upgrades them all, and they can never drift onto different versions.
  • Cons: each call makes an extra call to the beacon, and you can’t upgrade one instance without the others. A bad upgrade hits every instance at the same moment.

Side by side

Transparent UUPS Beacon
Upgrade logic lives in Proxy (via ProxyAdmin) Implementation Beacon
Per-call overhead Admin check Lowest Extra call to the beacon
Upgrade many at once No No Yes
Main risk Operational overhead Broken or unguarded upgrade function One bad upgrade hits all instances
Good fit Simple singletons Core singleton contracts Factory-deployed instances

Mistakes that apply to all of them

Uninitialised implementations. Implementations are deployed contracts in their own right. If their initialiser can be called, someone can take ownership of the implementation itself. Call _disableInitializers() in the constructor.

Storage layout changes. A new implementation must keep every existing storage variable in the same slot. Reordering, removing or changing the type of a variable corrupts state. Use ERC-7201 namespaced storage so each module keeps its variables in its own deterministic location, and compare layouts between versions in CI (for example with forge inspect <Contract> storage-layout or the OpenZeppelin upgrades tooling).

Constructors and immutables. Constructor logic runs on the implementation, not the proxy. Set state in initialisers. Immutables are fine, but they are baked into the implementation and change only with an upgrade.

Who can upgrade. An upgrade can change anything, so the key that controls it controls the protocol. Put upgrades behind a multisig and a timelock, publish the delay, and make sure users can exit before a malicious upgrade takes effect.

How to choose

A pattern we use often:

  • UUPS for core singletons, such as a registry, an oracle or a stablecoin, with upgrades authorised through the protocol’s access manager and a timelock.
  • Beacons for anything a factory creates many of, such as markets, vaults or tranches, so every instance runs the same version.
  • Transparent proxies mainly when integrating with tooling or teams that expect them.

Whatever you choose, write it into your threat model: which contracts can be upgraded, by whom, how quickly, and what users can do about it.

If you’re designing a protocol’s upgrade architecture, we can help: see our Ethereum smart contract development service.

Have something you'd like built?

Tell us what you're building. We'll reply within two working days with questions, an approach and next steps.

Start a project
Esc

↑ ↓ to move · Enter to open