Proxies & Upgradeability

29.1 Why proxies?

Contract code is immutable. Proxies separate state (in the proxy) from logic (in an implementation contract). The proxy's fallback delegatecalls every call to the implementation; upgrading means pointing the proxy to a new implementation.

Trade-offs: upgradeability adds trust assumptions (whoever controls upgrades controls user funds), complexity, and storage-collision risks. Use timelocks, multisigs, and governance for upgrade rights — or prefer immutable contracts where possible.

User ──call──► Proxy (storage, balance) ──delegatecall──► Implementation V1 (code only)
                         └── upgrade: implementation = V2

29.2 A minimal proxy (for understanding — use audited libraries in production)

ERC-1967 standardizes slots for the implementation, admin (keccak256("eip1967.proxy.admin") - 1) and beacon, so explorers can detect proxies and slots don't collide with the implementation's variables.

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

contract MinimalProxy {
    // ERC-1967 implementation slot: bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
    bytes32 private constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    constructor(address impl) {
        assembly { sstore(IMPL_SLOT, impl) }
    }

    fallback() external payable {
        assembly {
            let impl := sload(IMPL_SLOT)
            calldatacopy(0, 0, calldatasize())
            let ok := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch ok
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    receive() external payable {}
}

29.3 Patterns compared

Upgrade logic lives in Transparent (OZ TransparentUpgradeableProxy) proxy + ProxyAdmin admin can't accidentally call implementation; simple impl more gas per call (admin check), extra ProxyAdmin contract

UUPS (ERC-1822 / OZ UUPSUpgradeable) implementation (upgradeToAndCall) cheaper calls, smaller proxy, can remove upgradeability forgetting _authorizeUpgrade or deploying a non-UUPS impl can brick or expose the proxy

Beacon (BeaconProxy + UpgradeableBeacon) upgrade many proxies in one tx extra call to beacon per call Diamond (EIP-2535) diamond (diamondCut) many facets, unlimited size, granular upgrades complex, harder to audit and verify Minimal clones (ERC-1167) none (not upgradeable) very cheap deployment

29.4 UUPS with OpenZeppelin

Deploy with ERC1967Proxy(address(new BoxV1()), abi.encodeCall(BoxV1.initialize, (owner, 42))), or with the OpenZeppelin Upgrades plugins for Foundry/Hardhat which also validate storage-layout compatibility.

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

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

contract BoxV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    uint256 public value;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() { _disableInitializers(); }        // lock the implementation itself

    function initialize(address owner_, uint256 v) public initializer {
        __Ownable_init(owner_);
        value = v;
    }

    function setValue(uint256 v) external onlyOwner { value = v; }

    function _authorizeUpgrade(address) internal override onlyOwner {}   // REQUIRED access control
}

contract BoxV2 is BoxV1 {
    function increment() external { value += 1; }     // new function; keep existing storage order
    function version() external pure returns (string memory) { return "2"; }
}

29.5 Upgradeable-contract rules

No constructors for state — use initializer functions; call _disableInitializers() in the implementation's constructor.

Protect initialize from being called twice or front-run (deploy and initialize atomically).

Never reorder, remove or change the type of existing state variables; only append (or use ERC-7201 namespaces).

Don't use immutable for per-proxy values (immutables live in implementation code) unless intended.

No selfdestruct or delegatecall to arbitrary addresses in implementations.

Use reinitializer(n) for migration steps in new versions.

29.6 Diamond (EIP-2535) overview

A diamond maps each function selector to a facet address:

Facets share storage through "diamond storage" or "AppStorage" structs at fixed slots. Reference implementations: Nick Mudge's diamond-1/2/3.

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

library LibDiamond {
    bytes32 constant STORAGE_POSITION = keccak256("diamond.standard.diamond.storage");

    struct DiamondStorage {
        mapping(bytes4 => address) selectorToFacet;
        address owner;
    }

    function diamondStorage() internal pure returns (DiamondStorage storage ds) {
        bytes32 position = STORAGE_POSITION;
        assembly { ds.slot := position }
    }
}

contract Diamond {
    constructor(address owner) { LibDiamond.diamondStorage().owner = owner; }

    fallback() external payable {
        address facet = LibDiamond.diamondStorage().selectorToFacet[msg.sig];
        require(facet != address(0), "Diamond: function does not exist");
        assembly {
            calldatacopy(0, 0, calldatasize())
            let ok := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch ok
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
    // A full diamond adds diamondCut(FacetCut[], address init, bytes calldata) and loupe functions
    // (facets(), facetAddress
…