Skip to content

Proxies and upgrades

A proxy is a small contract at a stable address that keeps the data and the money and forwards every call, with delegatecall, to a separate contract that holds the code. An upgrade changes one stored address. Whoever can change it controls everything behind the proxy.

The problem: code at an address can never change. Users, tokens and other contracts all point at that address, so you cannot deploy v2 somewhere else and expect anyone to follow.

The fix: put a tiny forwarder, the proxy, at the address everyone knows. It forwards every call to a separate contract holding the real code, the implementation. To upgrade, you point the proxy at new code. It is a k8s Service with a stable name in front of a Deployment whose image you swap, except that here the Service holds the database.

The catch: whoever can do the swap controls every dollar behind the proxy. The pattern solved immutability and created a new crown jewel.

The patterns were standardised one by one: ERC-1822 (UUPS) and ERC-1967 (fixed storage slots) in 2019, ERC-2535 (diamonds) in 2020, ERC-7201 (namespaced storage) in 2023.

  1. A user or another contract calls the proxy’s address.
  2. The proxy has almost no functions of its own, so the call lands in its fallback().
  3. The fallback reads the implementation address from a fixed storage slot and does a delegatecall to it with the same calldata.
  4. delegatecall runs the implementation’s code on the proxy’s storage, with the original msg.sender. Balances and state live in the proxy. The implementation’s own storage is never used.
  5. The result is returned to the caller as if the proxy had run the code itself.
  6. An upgrade writes a new implementation address into that slot and emits an Upgraded event.
User / contract call Proxystable addressdata + money Implementation v1 Implementation v2 before delegatecall
An upgrade changes one stored address inside the proxy. Users, balances and the contract's address stay the same. Only the code behind it changes.

If the proxy kept the implementation address in slot 0, it would collide with the implementation’s first variable. ERC-1967 puts it in slots derived from a hash, far away from normal variables.

Slot Value Derived from
Implementation 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc keccak256("eip1967.proxy.implementation") - 1
Beacon 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50 keccak256("eip1967.proxy.beacon") - 1
Admin 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 keccak256("eip1967.proxy.admin") - 1

A constructor runs once, on the implementation’s own storage, when the implementation is deployed. The proxy never sees it. So upgradeable contracts move setup into an ordinary function, usually initialize(), guarded so it can run only once. That function must be called on the proxy, and must not stay callable on the implementation. OpenZeppelin’s _disableInitializers() in the implementation’s constructor locks the implementation.

Variables map to slots in declaration order. Version 2 runs on the storage that version 1 wrote. If v2 reorders, removes or inserts a variable, it reads v1’s data under the wrong name, and balances read as garbage.

  • Append only: new variables go after the old ones.
  • ERC-7201 namespaced storage: each module keeps its variables in a struct at a slot computed from a namespace string, so modules and versions cannot collide. The slot is keccak256(abi.encode(uint256(keccak256(bytes(id))) - 1)) & ~bytes32(uint256(0xff)).
Transparent UUPS Beacon Diamond User Admin Proxydata + upgradeTo() delegatecall Implementation power: proxy admin User Proxydata only delegatecall Implementationcode + upgrade() power: upgrade() in code Acct 1 Acct 2 Acct N Beaconsays: run v2 which code Implementation 1 change = every account User Diamondselector → facet Borrow Trade Admin power: diamondCut()
Same idea, four shapes. Red marks who holds the upgrade power: the thing an attacker wants, and the thing to put behind multisig, timelock and monitoring. A common combination is beacons for a fleet of per-client accounts and diamonds for the large core contracts.
  • Transparent proxy. The upgrade function lives in the proxy. “Transparent” because ordinary users never see it: calls from the admin go to the upgrade logic, everyone else is passed straight through. Simple and safe, slightly more gas.
  • UUPS. Universal Upgradeable Proxy Standard (ERC-1822). The upgrade function lives in the implementation code instead, so the proxy is lighter. Risk: ship a version that forgets the upgrade function, or checks access wrongly, and the contract is frozen forever or anyone can take it over.
  • Beacon proxy. Hundreds of proxies ask one beacon contract “which code do I run?”. Change the beacon once, and every proxy upgrades. Like many Deployments pointing at one image tag you retag. A good fit for one-per-client accounts. The blast radius is all of them.
  • Diamond (ERC-2535). One proxy, many code modules (facets), and a routing table mapping each function selector to its facet. Like an API gateway routing /borrow and /trade to different services that share one database. diamondCut edits the routing table. Risks: a bad new facet (LI.FI 2024), two facets clashing on the same function or data.
Pattern Where the upgrade logic lives Who holds the power One upgrade changes Cost Pick it when Main risk
Transparent In the proxy The proxy admin One proxy Slightly more gas per call You want the simplest model Admin key
UUPS (ERC-1822) In the implementation Whoever passes the check inside upgrade() One proxy Lighter proxy You want cheaper calls and accept more care per release A release without a working upgrade function freezes or exposes the contract
Beacon In a shared beacon contract The beacon owner Every proxy that reads the beacon One extra read per call Many identical instances, such as one account per client Blast radius is the whole fleet
Diamond (ERC-2535) diamondCut on the diamond Whoever may call diamondCut Any subset of functions Routing table lookup; more complexity to review One large system split across many modules A bad facet; selector or storage clashes between facets
No proxy (immutable) Nowhere Nobody Nothing None The logic is small and must be trusted not to change Bugs can only be fixed by migrating users to a new address
Risk What goes wrong Control
Storage collision A new implementation reorders or inserts variables; balances read as garbage. Append-only layout, ERC-7201 namespaced storage, storage-layout diff in CI (forge inspect ... storageLayout, OpenZeppelin upgrades plugin).
Uninitialised implementation Anyone calls initialize on the implementation and becomes owner; with UUPS they can then self-destruct or brick it. _disableInitializers() in the constructor; deploy and initialise atomically.
Initialiser front-run The proxy is deployed in one transaction and initialised in another; an attacker initialises first. A factory deploys and initialises in one transaction.
Upgrade key One key or a small multisig can replace all logic (Drift 2026 admin transfer). Multisig plus timelock plus public queue plus monitoring of Upgraded and DiamondCut; a separate pause role that cannot upgrade.
Selector clash in a diamond Two facets define the same function, or share a storage position. Selector-clash check in CI, facet audit before diamondCut, an alert on every cut.

Commands that show the current state. They use cast and forge from Foundry and assume --rpc-url or ETH_RPC_URL is set.

Terminal window
# Which implementation does this proxy run right now?
cast implementation $PROXY
# Who is the ERC-1967 admin?
cast admin $PROXY
# Read the implementation slot directly
cast storage $PROXY 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
# Storage layout of a contract in the current build, to diff against the previous release
forge inspect MyContract storageLayout

Watch these events in monitoring: Upgraded(address) on ERC-1967 proxies and beacons, AdminChanged, DiamondCut on diamonds, OwnershipTransferred and RoleGranted on whatever holds the upgrade right. See Defence stack and, for the keys behind the upgrade right, Key and signer attacks.

proxy, implementation, logic contract, delegatecall, fallback, upgrade, upgradeTo, upgradeToAndCall, Upgraded event, transparent proxy, proxy admin, UUPS, ERC-1822, beacon proxy, UpgradeableBeacon, diamond, facet, diamondCut, ERC-2535, selector clash, ERC-1967, implementation slot, storage collision, storage layout, append-only storage, storage gap, ERC-7201, namespaced storage, initializer, initialize, _disableInitializers, initialiser front-running, uninitialised implementation, timelock, multisig, pause role, forge inspect, cast implementation, OpenZeppelin upgrades plugin.