Back to Blog
Developer GuidesMay 4, 2026by Theo Nova

Upgradeable Contracts Done Right: Proxy Patterns Explained

Upgradeable Contracts Done Right: Proxy Patterns Explained

Upgradeable Contracts Done Right: Proxy Patterns Explained

Upgradeability gives you flexibility, but done wrong, it's a security liability. This guide breaks down the three main proxy patterns in Solidity, when to use each, and the common mistakes that turn upgradeable contracts into vulnerabilities.

Why Upgradeability Is a Double-Edged Sword

Smart contracts are immutable by default. Once deployed, the code at a given address cannot change, and for many use cases, that's a feature, not a limitation. Immutability is a trust guarantee.

But protocols evolve. Bugs get discovered. Business logic needs updating. Features get added. The demand for upgradeable contracts is real, and proxy patterns are the standard mechanism for delivering them.

The catch: upgradeability introduces centralization and complexity. An upgradeable contract is only as trustworthy as the team (or governance mechanism) controlling the upgrade key. And if the upgrade mechanism itself is implemented incorrectly, it becomes an attack surface. Storage collisions have drained protocols. Unprotected upgrade functions have handed attackers full control.

Used correctly, proxy patterns are powerful. Used carelessly, they're a liability. Here's how to use them correctly.

How Proxy Patterns Work: delegatecall and Storage Slots

All proxy patterns are built on a single EVM primitive: delegatecall.

When contract A delegatecalls contract B, B's code executes in A's context, meaning B reads from and writes to A's storage, not its own. A's address, A's ETH balance, A's storage.

// Simplified proxy

contract Proxy {

address public implementation;

fallback() external payable {

address impl = implementation;

assembly {

calldatacopy(0, 0, calldatasize())

let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)

returndatacopy(0, 0, returndatasize())

switch result

case 0 { revert(0, returndatasize()) }

default { return(0, returndatasize()) }

}

}

}

The proxy holds the storage and ETH. The implementation contract holds the logic. To upgrade, you point the proxy at a new implementation, storage persists, logic changes.

Transparent Proxy Pattern: How It Works, Pros and Cons

The transparent proxy pattern (popularized by OpenZeppelin's early upgrade tooling) solves a specific problem: function selector clashes between the proxy and the implementation.

If both the proxy and implementation define a function with the same selector, which one runs? The transparent proxy resolves this by routing calls based on who is calling:

If the admin calls the proxy, they interact with the proxy's admin functions (upgrade, change admin)

If anyone else calls the proxy, the call is always delegated to the implementation

Pros

Clear separation of admin and user interactions

Well-understood, battle-tested pattern

Strong tooling support

Cons

Every non-admin call incurs a storage read to check the caller's identity, adds gas overhead

Admin cannot interact with the implementation through the proxy (must use a separate address)

More complex deployment and management

// OpenZeppelin TransparentUpgradeableProxy

TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(

address(implementation),

address(proxyAdmin),

initData

);

UUPS: Leaner and Safer

The Universal Upgradeable Proxy Standard (EIP-1822) moves the upgrade logic into the implementation contract, not the proxy. The proxy itself is minimal, just a delegatecall forwarder with no admin logic.

// UUPS Implementation

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract MyContractV1 is UUPSUpgradeable, OwnableUpgradeable {

function initialize(address initialOwner) public initializer {

__Ownable_init(initialOwner);

__UUPSUpgradeable_init();

}

function _authorizeUpgrade(address newImplementation)

internal override onlyOwner {}

}

Pros

Lower gas overhead, no admin check on every call

Smaller, simpler proxy bytecode

Upgrade authorization is fully customizable in the implementation

Cons

Critical risk: if you deploy a new implementation that accidentally removes or breaks _authorizeUpgrade, you permanently lose the ability to upgrade. This has happened in production.

More responsibility on the developer to implement authorization correctly

When to Use UUPS

UUPS is the recommended default for most new upgradeable contracts in 2026. It's leaner and puts authorization where it belongs, in the contract logic, but requires careful testing of every new implementation before deployment.

Beacon Proxy: Managing Multiple Instances From One Upgrade Point

The beacon proxy pattern is designed for protocols that deploy many instances of the same contract, lending pools, LP positions, vaults, and need to upgrade all of them simultaneously.

Rather than each proxy pointing directly at an implementation, every proxy points to a beacon contract. The beacon holds the current implementation address. Upgrade the beacon once, all proxies are upgraded.

// Deploy beacon

UpgradeableBeacon beacon = new UpgradeableBeacon(address(implementation), owner);

// Deploy multiple proxy instances

BeaconProxy proxy1 = new BeaconProxy(address(beacon), initData1);

BeaconProxy proxy2 = new BeaconProxy(address(beacon), initData2);

// Upgrade all instances at once

beacon.upgradeTo(address(newImplementation));

When to Use Beacon Proxy

Use beacon proxies when you have a factory pattern deploying many identical (or near-identical) contracts and want to maintain upgrade control over all of them centrally. Uniswap v3 pools use a variant of this pattern.

Storage Collision Risks and How to Prevent Them

Storage collision is the most dangerous failure mode in upgradeable contracts. If the proxy and implementation accidentally use the same storage slot for different variables, they will corrupt each other's data.

The solution is EIP-1967 standard storage slots, deterministic, hash-derived slot locations that are astronomically unlikely to collide with implementation storage:

// EIP-1967: Implementation slot

bytes32 private constant _IMPLEMENTATION_SLOT =

0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

OpenZeppelin's proxy contracts use these slots by default. If you're writing a custom proxy, use them.

When upgrading to a new implementation, never reorder or remove existing state variables. You can add new variables at the end of the existing layout, but changing the order of existing variables will map them to the wrong storage slots.

Initializers vs Constructors in Upgradeable Contracts

Constructors don't work in proxy patterns, the constructor runs when the implementation contract is deployed, not when the proxy is deployed, and it writes to the implementation's storage (which is irrelevant). Use initializer functions instead:

// Wrong, constructor runs in implementation context

constructor(address owner) {

_transferOwnership(owner);

}

// Correct, initializer runs in proxy context

function initialize(address owner) public initializer {

__Ownable_init(owner);

}

The initializer modifier (from OpenZeppelin) ensures the function can only be called once. Always call it. An uninitialized proxy is an open door, anyone can call initialize and take ownership.

When NOT to Use Upgradeable Contracts

Upgradeability is a trust tradeoff. Your users are trusting that you won't upgrade the contract maliciously. Before reaching for a proxy pattern, ask:

Is immutability a stronger guarantee for your users? Simple token contracts, escrow contracts, and NFTs often benefit more from immutability than upgradeability.

Can you use modular design instead? Many "upgrade needs" can be addressed by separating concerns into multiple contracts with clear interfaces.

Do you have proper governance? An upgradeable contract controlled by a single EOA provides weaker guarantees than an immutable contract.

If you do use upgradeable contracts, implement timelocks on upgrade functions so users have time to exit before changes take effect.

OpenZeppelin Tooling and Best Practices

The OpenZeppelin Upgrades Plugin for Hardhat and Foundry is the standard tool for managing upgradeable contract deployments. It:

Validates storage layout compatibility before upgrades

Manages deployment addresses across networks

Generates upgrade manifests for auditability

# Hardhat

npx hardhat run scripts/deploy.js, network sepolia

# Foundry (via oz-foundry-upgrades)

forge script script/Deploy.s.sol, broadcast

Always run validateUpgrade in your upgrade script before broadcasting to mainnet. The tooling will catch storage layout violations before they become production incidents.

Key Takeaways

  • Proxy patterns use delegatecall to separate a contract's logic from its state, enabling upgrades without redeployment.
  • UUPS proxies are leaner and safer than transparent proxies because the upgrade logic lives in the implementation contract.
  • Beacon proxies let you upgrade many contract instances from a single point, ideal for factory patterns.
  • Storage collisions are the most dangerous pitfall; always use storage gap patterns and append-only state variables.
  • Not every contract needs upgradeability. If your contract is simple and correct, immutability is a stronger trust signal.

Conclusion: Choosing the Right Pattern for Your Project

The right proxy pattern depends on your specific requirements:

UUPS, best default for most protocols. Lean, flexible, puts authorization in your control.

Transparent Proxy, good for teams that want strict admin/user separation and are willing to pay the gas overhead.

Beacon Proxy, purpose-built for factory patterns deploying many instances of the same logic.

Whatever pattern you choose, pair it with governance (multisig + timelock), storage layout validation, and a tested upgrade path.

Autheo helps you manage the operational complexity of upgradeable contracts across environments, tracking which implementation is live on which network, who deployed it, and when. Because knowing what you deployed is just as important as knowing how to upgrade it.

Share

Gear Up with Autheo

Rep the network. Official merch from the Autheo Store.

Visit the Autheo Store

Theo Nova

The editorial voice of Autheo

Research-driven coverage of Layer-0 infrastructure, decentralized AI, and the integration era of Web3. Written and reviewed by the Autheo content and engineering teams.

About this author →

Get the Autheo Daily

Blockchain insights, AI trends, and Web3 infrastructure updates delivered to your inbox every morning.