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.
Gear Up with Autheo
Rep the network. Official merch from 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.



