Smart contracts are often described as immutable, yet many production systems need a path to fix defects, change parameters, add features, or respond to external protocol upgrades. EVM proxy patterns create that path by keeping a stable user-facing address and storage context while routing calls to replaceable implementation code.

Upgradeability is not free mutability. It adds a privileged control plane, storage-compatibility rules, initialization hazards, selector boundaries, and governance decisions. A safe design treats the upgrade mechanism as part of the protocol and documents who can change code, how quickly, under which checks, and how users can observe or exit.

The proxy separates address, storage, and code

Users call the proxy address. The proxy's fallback logic forwards calldata to an implementation with DELEGATECALL. The implementation's code executes, but storage reads and writes apply to the proxy. The proxy remains the holder of balances, approvals, roles, and persistent variables while the implementation address can change.

The OpenZeppelin proxy pattern documentation explains this separation and the unstructured-storage approach used to avoid collisions between proxy control variables and application storage. The underlying rule is simple: implementation code controls the proxy's state.

That rule is also the main risk. A malicious or buggy implementation can overwrite roles, transfer assets, destroy assumptions, or make future upgrades impossible. Protecting the upgrade key is necessary, but verifying implementation behavior is equally important.

Implementation slots must not collide with application state

A proxy needs to store the current implementation address and often an administrator or beacon reference. If those values occupy ordinary low-numbered slots, application variables in the implementation can overwrite them during delegated execution. Standardized slots derive locations from unique hashes and place control data away from compiler-assigned application slots.

Standards improve interoperability with explorers and tools, but they do not eliminate all storage risk. The implementation still needs a stable layout across versions. The proxy can safely store its own control fields while a new implementation corrupts old user data through a reordered variable list.

Review the actual deployed slot values after every upgrade. A UI that shows a new implementation address is helpful, but reading the standardized slot and comparing the code hash provides a stronger check.

Storage layout is an append-only contract

Compilers assign state variables to slots in declaration order, packing some smaller values into the same word. Mappings and dynamic arrays use hashed locations derived from a base slot. Changing declaration order, type width, inheritance order, or struct layout can reinterpret existing bytes as different variables.

A common safe rule is to preserve every existing variable in place and append new variables at the end. Reserved storage gaps can leave room in base contracts for future additions, though gaps must be maintained correctly. Namespaces are another strategy that groups variables under explicitly derived slots.

Automated layout comparison should be part of the release build. It should compare fully qualified contracts, inherited variables, type definitions, and slot offsets. A tool warning is not proof of safety, but ignoring a warning without a written explanation is a strong signal that the upgrade process is too informal.

Constructors do not initialize proxy storage

An implementation constructor runs when the implementation contract is deployed, using the implementation's own storage context. Calls through the proxy never execute that constructor against proxy storage. Upgradeable contracts therefore move initialization into an external or public initializer that runs through the proxy.

The initializer must be callable exactly as intended. If an attacker can initialize an unconfigured proxy first, they may acquire ownership or roles. If a reinitializer can be called twice, state can be reset. Deployment scripts should create the proxy and initialize it atomically when possible, then assert every critical field.

Implementation contracts themselves should usually be locked against initialization so nobody can take control of the standalone implementation and misuse functions that depend on its own state. The threat depends on code behavior, but leaving implementations openly initializable is unnecessary ambiguity.

Transparent, UUPS, beacon, and minimal proxies

Transparent proxy

A transparent proxy distinguishes admin calls from user calls. The admin can invoke upgrade functions on the proxy, while ordinary callers are delegated to the implementation. This avoids selector clashes at the cost of a separate admin path and contract.

UUPS-style proxy

A UUPS proxy keeps upgrade logic in the implementation. The proxy is smaller, but every new implementation must preserve a valid upgrade function and authorization check. An incorrect implementation can remove the upgrade path or make it callable by anyone.

Beacon proxy

Multiple proxies read their implementation from a shared beacon. Updating the beacon changes code for the entire fleet. This is efficient for many similar instances but increases blast radius and requires careful compatibility across all proxy states.

Minimal clone

A minimal proxy delegates to a fixed implementation and is typically not independently upgradeable. It is useful for cheap instance deployment. Upgrade behavior, if any, lives in an external registry, beacon-like layer, or migration process rather than the clone itself.

Selector and call-context hazards

A proxy has its own functions and also forwards arbitrary calldata. Function selectors are only four bytes, so different signatures can collide. Transparent proxy dispatch rules reduce one class of conflict, but interfaces and tooling should still identify which address and ABI are being used.

msg.sender and msg.value are preserved under delegate call, while address(this) is the proxy. This is what makes implementation logic behave like the proxy, but it can surprise code that expects to know its own deployed address. Immutable variables are embedded in implementation bytecode and therefore shared by all proxies using that implementation.

Self-calls, reentrancy guards, domain separators, signature verification, and contract-address checks should be tested through the proxy path. A test that calls the implementation directly can pass while the real proxy context fails.

Governance defines the real upgrade policy

The technical ability to upgrade is only half the design. The governance path might be a single key, multisignature, timelock, on-chain vote, security council, or layered process. Document the signer threshold, delay, cancellation path, emergency authority, and conditions under which normal process can be bypassed.

Timelocks give users and monitors time to inspect a proposed implementation. They are less useful when the proposal contains an opaque address without verified source, reproducible bytecode, or release notes. Monitoring should decode the implementation change, code hash, initializer call, and any simultaneous role changes.

Emergency upgrades may reduce loss during an active exploit, but broad emergency power is also a trust assumption. Consider scoped pause functions, rate limits, and staged controls that can contain damage without immediately replacing arbitrary code.

Test the upgrade as a state migration

An upgrade test should begin from a realistic old deployment with nontrivial users, balances, roles, mappings, arrays, and edge-case values. Apply the exact governance and proxy calls planned for production. Then verify old behavior that must remain, new behavior, storage values, events, permissions, and rollback assumptions.

Fork tests are valuable when the target proxy already exists. Pin a recent block, impersonate the authorized governance path where appropriate, simulate the upgrade, and run application-level scenarios against real state. Do not broadcast from the same script configuration used for simulation without an explicit environment guard.

Save the pre-upgrade and post-upgrade implementation addresses, code hashes, layout reports, call data, and transaction simulation. Those artifacts are part of the change record.

Rollback is not automatic

Pointing the proxy back to an old implementation may not restore safety. The new code may have written state the old code cannot interpret, changed external dependencies, or executed irreversible transfers. A “rollback” should be tested as its own state transition from plausible partially upgraded conditions.

Safer releases can separate storage migration from feature activation, add a pause, cap exposure, or use a staged rollout across instances. For high-impact systems, define which state changes make rollback impossible and what replacement plan exists after that point.

Upgrade review checklist

  • Identify the proxy pattern and the exact storage slots holding implementation and admin data.
  • Compare old and new storage layouts, inheritance order, structs, enums, and variable widths.
  • Verify initializer and reinitializer guards, arguments, and atomic deployment behavior.
  • Test every privileged function through the proxy, not only on the implementation.
  • Confirm the new implementation preserves upgrade authorization and future upgrade capability.
  • Simulate with realistic state and a pinned fork when an existing deployment is involved.
  • Document governance threshold, timelock, emergency powers, monitoring, and cancellation.
  • Define rollback limits and post-upgrade assertions before execution.

Conclusion

Proxy upgrades work by separating stable address and storage from replaceable code. That architecture can support responsible maintenance, but it turns storage layout, initialization, delegate-call context, and governance into permanent security concerns. The upgrade mechanism should be easier to inspect than the application it controls.

Use the local EVM stack guide to simulate upgrades and the security review guide to assess the wider trust boundary.