100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Upgradeable Contracts and Proxies

How proxy patterns let you change contract logic after deployment using delegatecall, the difference between Transparent and UUPS proxies, and the storage-layout and initializer rules that keep upgrades safe.

Practical SolidityAdvanced11 min readJul 10, 2026
Analogies

Why Upgradeable Contracts Exist

Immutability is a security feature, but it is also a liability: a discovered bug or a needed feature cannot be applied to an already-deployed contract. Upgradeable contracts restore the ability to change logic after deployment, at the cost of added complexity and a new trust assumption, since whoever controls the upgrade can change the rules under users' feet, so upgrade rights are typically held by a multisig or governance contract rather than a single key. The dominant technique is the proxy pattern: users always interact with a stable proxy address whose code can be pointed at successive implementation contracts.

🏏

Cricket analogy: It's like a captain who can change the bowling plan mid-innings, flexible and powerful, but that authority must be trusted not to be misused, so it rests with the captain, not any one fielder.

How Proxies Work: delegatecall

A proxy contract stores all the state and holds the user-facing address, but contains almost no business logic. When a call arrives, its fallback function uses delegatecall to run the code of a separate implementation contract in the proxy's own storage context. delegatecall is the key primitive: it executes the target's bytecode but reads and writes the caller's (proxy's) storage, and preserves msg.sender and msg.value. To upgrade, you simply store a new implementation address; the proxy's storage, and thus all user balances and state, stays intact while the logic changes.

🏏

Cricket analogy: It's like a batter following instructions relayed from the dressing room but playing with their own bat and stance, so the strategy comes from elsewhere yet executes on the player's own body.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

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

contract CounterV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 public count;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers(); // stop anyone initializing the logic contract itself
    }

    function initialize(address owner_) public initializer {
        __Ownable_init(owner_);
        __UUPSUpgradeable_init();
    }

    function increment() external {
        count += 1;
    }

    // Only the owner may authorize an upgrade to a new implementation
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

Transparent vs UUPS Proxies

Two proxy patterns dominate. In the Transparent Proxy pattern, the proxy itself contains the upgrade logic and an admin address; it routes admin calls to upgrade functions and everyone else's calls to the implementation, avoiding function-selector clashes. In the newer UUPS (Universal Upgradeable Proxy Standard) pattern, the upgrade function lives in the implementation contract instead, making the proxy smaller and cheaper to deploy, but with a critical catch: if you ever upgrade to an implementation that omits the upgrade logic, the contract becomes permanently frozen. OpenZeppelin's Upgrades plugins automate deploying both and run safety checks against common mistakes.

🏏

Cricket analogy: It's like choosing whether the review authority sits with the on-field umpire or is delegated to each new third umpire, so if a replacement third umpire lacks the tools, reviews stop entirely.

Storage Layout and the Initializer Trap

Because a proxy runs new logic against old storage, storage-layout compatibility is the deepest hazard. Storage variables occupy fixed slots in declaration order; a new implementation must keep every existing variable in the same slot and only append new ones, since reordering, inserting, or changing a variable's type corrupts existing data. Two practices manage this: reserving uint256[50] private __gap; slots for future variables, and, because constructors do not run on the proxy's storage, using an initializer function (guarded so it runs exactly once) instead of a constructor to set initial state.

🏏

Cricket analogy: It's like a batting order fixed by numbered positions, where you can add a new player at the end but must never reshuffle existing spots, or every player's stats attach to the wrong slot.

Upgradeable contracts cannot use constructors to initialize state, because the constructor runs in the implementation's context, not the proxy's, and must not rely on immutable/constant state persisting in the proxy. Use an initializer guarded by the initializer modifier so it runs exactly once, and call _disableInitializers() in the implementation's constructor to stop anyone initializing the logic contract directly.

OpenZeppelin's Hardhat Upgrades plugin (deployProxy/upgradeProxy) automatically deploys the implementation and proxy, wires up the initializer, and runs storage-layout compatibility checks between versions, flagging unsafe changes before you deploy a broken upgrade.

  • Upgradeability trades immutability's safety for the ability to fix bugs and add features, adding a trust assumption in whoever controls upgrades.
  • A proxy holds the stable address and all storage; delegatecall runs a separate implementation's logic in the proxy's storage context.
  • Transparent proxies keep upgrade logic and an admin in the proxy; UUPS puts upgrade logic in the implementation for a cheaper proxy.
  • UUPS risks permanent freezing if an upgrade removes the upgrade function.
  • Storage layout must be append-only: never reorder, insert, or retype existing variables, or you corrupt state.
  • Reserve __gap slots for future variables and use a guarded initializer instead of a constructor.
  • OpenZeppelin Upgrades plugins automate deployment and run storage-safety checks between versions.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#SolidityStudyNotes#UpgradeableContractsAndProxies#Upgradeable#Contracts#Proxies#Exist#StudyNotes#SkillVeris#ExamPrep