Smart Contracts

Upgradeable Smart Contracts: Patterns and Pitfalls

Transparent proxies, UUPS, diamonds, and Beacons – how each upgrade pattern handles storage, admin authority, and migration; where each pattern has cost protocols real money.

Red Asgard Research·Updated May 2026

Why upgradeable contracts are dangerous

Upgradeability is the right answer to: "we will need to fix bugs in production." It is also the right answer to: "we will need to drain the protocol."

Every proxy pattern has the same shape: a contract whose storage is controlled by one address and whose code is set by another. Whoever can change the code can change the meaning of every state variable. Whoever can change the admin can change who can change the code.

The audit work below is about which addresses can do which thing, and what stops them from doing the wrong thing.

This paper focuses on EVM proxy patterns. Upgrade authority models differ across runtimes; on Cairo upgrades happen through class-hash replacement, on Solana through BPFLoaderUpgradeable with a separate authority, on Move through module-republish under governance, on CosmWasm through migrate handlers, and on TON through code-cell mutation. The methodology applies; the specific mechanism does not.

The proxy patterns

Transparent proxy. Storage in the proxy. Code in the implementation. Admin in the proxy. The proxy distinguishes between admin calls (handled in the proxy itself) and user calls (delegatecalled to the implementation). The implementation has no upgrade authority; only the admin does.

UUPS (Universal Upgradeable Proxy Standard). Storage in the proxy. Code in the implementation. Upgrade logic in the implementation. The proxy is a thin shell. The implementation contains the function that upgrades the implementation, which the admin (also stored on the implementation side via inherited contracts) calls. Compact but moves upgrade authority into the code being upgraded.

Beacon. Storage in each proxy. Code reference is a beacon contract; proxies read the beacon to find the implementation. One beacon update affects all proxies pointing at it. Useful when many proxies share an implementation.

Diamond (EIP-2535). Storage in the diamond. Multiple implementations, called "facets," each handling a subset of function selectors. Proxies route calls by function selector to the right facet. Used for contracts large enough that a single implementation hits contract-size limits. Significantly more complex; the storage layout discipline is correspondingly tighter.

The Diamond's diamondCut function is the upgrade primitive: it adds, replaces, or removes facets and the function selectors they handle. Whoever controls diamondCut controls every function in the diamond. The audit question on every diamond is what gates diamondCut and what authority can grant diamondCut rights to a new address.

Factory-deployed proxies. A single factory contract that can deploy many proxies. The pattern is common in DEX router clones, vault factories, NFT-mint factories. The factory holds the implementation reference and the deployment authority. Compromising the factory compromises every proxy it has ever deployed and every future one. Audit question: who controls the factory, can the implementation reference be changed, and what is the blast radius if the factory is wrong?

Initialization mistakes

The most consistent failure mode across all proxy patterns is initialization.

Constructors do not initialize proxy storage. Constructors run during deployment of the implementation, against the implementation's storage. Proxy storage is empty until something runs against the proxy.

Initialization must run exactly once against the proxy. The standard pattern is an initialize() function with a one-shot guard (initializer modifier from OpenZeppelin) that blocks re-entry.

Implementation contracts must disable direct initialization. If the implementation is never initialized, an attacker can initialize it directly, take ownership of the implementation, and then attack downstream proxies that share dependencies.

The failure mode is not one bug. An uninitialized proxy can be taken over by whoever calls initialize() first. An initialized implementation can expose UUPS or ownership hazards. A reinitializer can reopen privileged configuration after it should have been closed.

We check deployment scripts, initializer guards, implementation constructors or _disableInitializers() equivalents, proxy admin paths, and on-chain logs.

Storage layout migration

A proxy upgrade preserves storage. The new implementation must agree with the old implementation about which slot holds which variable.

The rules:

  • Append new state variables to the end. Never insert in the middle.
  • Reserve gap slots in inherited contracts. OpenZeppelin upgradeable contracts include __gap arrays for this reason; the gap closes as new variables are added in the parent.
  • Never reorder existing variables.
  • Never delete a variable; it stays as a dead slot.
  • When inheriting from an upgraded base, audit the gap.

Storage layout audits diff the layout of v_new against v_old and flag every collision, gap close, type change, and slot shift. The work is mechanical; the failure to do the work is what kills protocols.

ERC-7201 (2024) introduced namespaced storage as a standard pattern for upgradeable contracts. Storage variables are placed at deterministic hashed slots rather than sequential slots, eliminating the gap-management problem. OpenZeppelin v5 upgradeable contracts adopted ERC-7201; legacy contracts using sequential storage still need gap audits. The audit-time question shifts from "does the gap discipline hold" to "are the namespaced slots stable across the upgrade."

Admin path security

The proxy admin is the keyholder. The audit work on the admin path is more demanding than the audit work on the implementation.

Who is the admin? A multisig? Whose multisig? With what threshold? With what custody on the keys?

Can the admin be changed? If yes, by whom, and under what time-lock? An admin-change function that runs without a time-lock means an attacker who compromises the admin owns the protocol immediately, with no public warning.

Are upgrade transactions time-locked? A time-lock of a few hours on upgrades gives the community a chance to react to a malicious upgrade. The time-lock has to be real; constructor-set time-locks that the same admin can change are not time-locks.

Is there an escape hatch for users? A protocol that locks user funds in an upgradeable contract has, in effect, made the admin a custodian. Some users want that. Most do not. The audit names it explicitly.

Is governance an attack surface? When the admin is a governance contract, the attack moves to governance: flash voting, low-quorum proposals, vote-buying. Governance audits are their own scope.

The 2022 Audius incident is the canonical example: the attacker exploited an initializer-related governance vulnerability to grant themselves voting power, then voted to mint and drain the treasury (approximately $6M moved before recovery). The vulnerability lived at the intersection of upgradeable-storage initialization and governance authority. Neither part was alone responsible.

The audit questions

At the start of an upgradeable-contract audit, we ask:

  • Which proxy pattern is in use, and why?
  • What is the storage layout, and what is the upgrade plan?
  • Who is the admin? What are the keys? How are they custodied?
  • Can the admin be rotated? On what time-lock?
  • Are upgrades time-locked?
  • Is the implementation initialization disabled?
  • Is the initializer protected against re-entry?
  • What is the escape hatch for users if the admin is compromised?

If the protocol cannot answer these questions, the audit answers them in the report. If the answers do not survive review, the report contains findings.

The methodology is in our auditing methodology paper.

What clean means

A clean upgradeable-contract audit means the proxy pattern is implemented correctly, the storage layout is documented and migrable, and the admin path has appropriate constraints. It does not mean the admin is trustworthy. The admin is whatever it is; the audit names them and constrains the technical surface around them.

Tags
smart-contractsproxy-patternsupgradeabilitysecurity