# Advanced Proxy And Upgrade Patterns

> Advanced Proxy & Upgrade Patterns

- Skill: `nickgallick/advanced-proxy-and-upgrade-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add nickgallick/advanced-proxy-and-upgrade-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nickgallick/advanced-proxy-and-upgrade-patterns/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: nickgallick (https://skillmd.com/u/nickgallick)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/nickgallick/advanced-proxy-and-upgrade-patterns

---

# Advanced Proxy & Upgrade Patterns

## Overview

Upgradeability in EVM smart contracts is not a single pattern but a spectrum of architectural choices with distinct tradeoffs in gas cost, security surface, governance complexity, and storage safety. This document covers the full landscape from minimal clones to multi-facet diamonds, with complete implementations and deployment tooling.

---

## 1. Diamond Pattern (EIP-2535)

The Diamond pattern solves two hard problems simultaneously: the 24KB contract size limit and the need for a single proxy address to serve multiple logical modules (facets).

### Core Concepts

- **Diamond**: The proxy contract at a single address. Stores a mapping from function selector to facet address.
- **Facet**: An implementation contract. Any number of facets can be attached to one diamond.
- **diamondCut**: The single function that adds, replaces, or removes facets atomically.
- **Loupe functions**: Introspection — enumerate facets, selectors, and facet addresses.
- **Diamond Storage**: Each facet uses a struct stored at a deterministic slot via `keccak256("diamond.storage.namespace") - 1` to avoid collisions.

### Diamond Storage Pattern

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

library DiamondStorageLib {
    // Each facet defines its own storage struct at a unique slot.
    // Slot formula: keccak256(abi.encode(uint256(keccak256("myapp.storage.token")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 constant TOKEN_STORAGE_SLOT =
        0x1234000000000000000000000000000000000000000000000000000000000000; // Replace with real hash

    struct TokenStorage {
        string name;
        string symbol;
        uint8 decimals;
        uint256 totalSupply;
        mapping(address => uint256) balances;
        mapping(address => mapping(address => uint256)) allowances;
    }

    function tokenStorage() internal pure returns (TokenStorage storage ts) {
        bytes32 slot = TOKEN_STORAGE_SLOT;
        assembly {
            ts.slot := slot
        }
    }
}
```

### DiamondCut Interface and Storage

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

interface IDiamondCut {
    enum FacetCutAction { Add, Replace, Remove }

    struct FacetCut {
        address facetAddress;
        FacetCutAction action;
        bytes4[] functionSelectors;
    }

    event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);

    function diamondCut(
        FacetCut[] calldata _diamondCut,
        address _init,
        bytes calldata _calldata
    ) external;
}

library LibDiamond {
    bytes32 constant DIAMOND_STORAGE_SLOT =
        keccak256(abi.encode(uint256(keccak256("eip2535.diamond.storage")) - 1)) & ~bytes32(uint256(0xff));

    struct FacetAddressAndPosition {
        address facetAddress;
        uint96 functionSelectorPosition; // position in facetFunctionSelectors.functionSelectors array
    }

    struct FacetFunctionSelectors {
        bytes4[] functionSelectors;
        uint256 facetAddressPosition; // position of facetAddress in facetAddresses array
    }

    struct DiamondStorage {
        // maps function selector => facet address and selector position
        mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition;
        // maps facet addresses to function selectors
        mapping(address => FacetFunctionSelectors) facetFunctionSelectors;
        // facet addresses
        address[] facetAddresses;
        // Used to query if a contract implements an interface.
        // Used to implement ERC-165.
        mapping(bytes4 => bool) supportedInterfaces;
        // owner of the contract
        address contractOwner;
    }

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

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    function setContractOwner(address _newOwner) internal {
        DiamondStorage storage ds = diamondStorage();
        address previousOwner = ds.contractOwner;
        ds.contractOwner = _newOwner;
        emit OwnershipTransferred(previousOwner, _newOwner);
    }

    function contractOwner() internal view returns (address contractOwner_) {
        contractOwner_ = diamondStorage().contractOwner;
    }

    function enforceIsContractOwner() internal view {
        require(msg.sender == diamondStorage().contractOwner, "LibDiamond: Must be contract owner");
    }

    event DiamondCut(IDiamondCut.FacetCut[] _diamondCut, address _init, bytes _calldata);

    function diamondCut(
        IDiamondCut.FacetCut[] memory _diamondCut,
        address _init,
        bytes memory _calldata
    ) internal {
        for (uint256 facetIndex; facetIndex < _diamondCut.length; facetIndex++) {
            IDiamondCut.FacetCutAction action = _diamondCut[facetIndex].action;
            if (action == IDiamondCut.FacetCutAction.Add) {
                addFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors);
            } else if (action == IDiamondCut.FacetCutAction.Replace) {
                replaceFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors);
            } else if (action == IDiamondCut.FacetCutAction.Remove) {
                removeFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors);
            } else {
                revert("LibDiamond: Incorrect FacetCutAction");
            }
        }
        emit DiamondCut(_diamondCut, _init, _calldata);
        initializeDiamondCut(_init, _calldata);
    }

    function addFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
        require(_functionSelectors.length > 0, "LibDiamond: No selectors in facet to cut");
        DiamondStorage storage ds = diamondStorage();
        require(_facetAddress != address(0), "LibDiamond: Add facet can't be address(0)");
        uint96 selectorPosition = uint96(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length);
        if (selectorPosition == 0) {
            addFacet(ds, _facetAddress);
        }
        for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
            bytes4 selector = _functionSelectors[selectorIndex];
            address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress;
            require(oldFacetAddress == address(0), "LibDiamond: Can't add function that already exists");
            addFunction(ds, selector, selectorPosition, _facetAddress);
            selectorPosition++;
        }
    }

    function replaceFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
        require(_functionSelectors.length > 0, "LibDiamond: No selectors in facet to cut");
        DiamondStorage storage ds = diamondStorage();
        require(_facetAddress != address(0), "LibDiamond: Add facet can't be address(0)");
        uint96 selectorPosition = uint96(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length);
        if (selectorPosition == 0) {
            addFacet(ds, _facetAddress);
        }
        for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
            bytes4 selector = _functionSelectors[selectorIndex];
            address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress;
            require(oldFacetAddress != _facetAddress, "LibDiamond: Can't replace function with same function");
            removeFunction(ds, oldFacetAddress, selector);
            addFunction(ds, selector, selectorPosition, _facetAddress);
            selectorPosition++;
        }
    }

    function removeFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
        require(_functionSelectors.length > 0, "LibDiamond: No selectors in facet to cut");
        DiamondStorage storage ds = diamondStorage();
        require(_facetAddress == address(0), "LibDiamond: Remove facet address must be address(0)");
        for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
            bytes4 selector = _functionSelectors[selectorIndex];
            address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress;
            removeFunction(ds, oldFacetAddress, selector);
        }
    }

    function addFacet(DiamondStorage storage ds, address _facetAddress) internal {
        enforceHasContractCode(_facetAddress, "LibDiamond: New facet has no code");
        ds.facetFunctionSelectors[_facetAddress].facetAddressPosition = ds.facetAddresses.length;
        ds.facetAddresses.push(_facetAddress);
    }

    function addFunction(DiamondStorage storage ds, bytes4 _selector, uint96 _selectorPosition, address _facetAddress) internal {
        ds.selectorToFacetAndPosition[_selector].functionSelectorPosition = _selectorPosition;
        ds.facetFunctionSelectors[_facetAddress].functionSelectors.push(_selector);
        ds.selectorToFacetAndPosition[_selector].facetAddress = _facetAddress;
    }

    function removeFunction(DiamondStorage storage ds, address _facetAddress, bytes4 _selector) internal {
        require(_facetAddress != address(0), "LibDiamond: Can't remove function that doesn't exist");
        require(_facetAddress != address(this), "LibDiamond: Can't remove immutable function");
        uint256 selectorPosition = ds.selectorToFacetAndPosition[_selector].functionSelectorPosition;
        uint256 lastSelectorPosition = ds.facetFunctionSelectors[_facetAddress].functionSelectors.length - 1;
        if (selectorPosition != lastSelectorPosition) {
            bytes4 lastSelector = ds.facetFunctionSelectors[_facetAddress].functionSelectors[lastSelectorPosition];
            ds.facetFunctionSelectors[_facetAddress].functionSelectors[selectorPosition] = lastSelector;
            ds.selectorToFacetAndPosition[lastSelector].functionSelectorPosition = uint96(selectorPosition);
        }
        ds.facetFunctionSelectors[_facetAddress].functionSelectors.pop();
        delete ds.selectorToFacetAndPosition[_selector];
        if (lastSelectorPosition == 0) {
            uint256 facetAddressPosition = ds.facetFunctionSelectors[_facetAddress].facetAddressPosition;
            uint256 lastFacetAddressPosition = ds.facetAddresses.length - 1;
            if (facetAddressPosition != lastFacetAddressPosition) {
                address lastFacetAddress = ds.facetAddresses[lastFacetAddressPosition];
                ds.facetAddresses[facetAddressPosition] = lastFacetAddress;
                ds.facetFunctionSelectors[lastFacetAddress].facetAddressPosition = facetAddressPosition;
            }
            ds.facetAddresses.pop();
            delete ds.facetFunctionSelectors[_facetAddress].facetAddressPosition;
        }
    }

    function initializeDiamondCut(address _init, bytes memory _calldata) internal {
        if (_init == address(0)) {
            return;
        }
        enforceHasContractCode(_init, "LibDiamond: _init address has no code");
        (bool success, bytes memory error) = _init.delegatecall(_calldata);
        if (!success) {
            if (error.length > 0) {
                assembly {
                    let returndata_size := mload(error)
                    revert(add(32, error), returndata_size)
                }
            } else {
                revert("LibDiamond: _init function reverted");
            }
        }
    }

    function enforceHasContractCode(address _contract, string memory _errorMessage) internal view {
        uint256 contractSize;
        assembly {
            contractSize := extcodesize(_contract)
        }
        require(contractSize > 0, _errorMessage);
    }
}
```

### Diamond Proxy Contract

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

import { LibDiamond } from "./LibDiamond.sol";
import { IDiamondCut } from "./IDiamondCut.sol";
import { IDiamondLoupe } from "./IDiamondLoupe.sol";

contract Diamond {
    constructor(address _contractOwner, address _diamondCutFacet) payable {
        LibDiamond.setContractOwner(_contractOwner);

        // Add the diamondCut external function from the diamondCutFacet.
        IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1);
        bytes4[] memory functionSelectors = new bytes4[](1);
        functionSelectors[0] = IDiamondCut.diamondCut.selector;
        cut[0] = IDiamondCut.FacetCut({
            facetAddress: _diamondCutFacet,
            action: IDiamondCut.FacetCutAction.Add,
            functionSelectors: functionSelectors
        });
        LibDiamond.diamondCut(cut, address(0), "");
    }

    // Find facet for function that is called and execute the function
    // if a facet is found and return any value.
    fallback() external payable {
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress;
        require(facet != address(0), "Diamond: Function does not exist");
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    receive() external payable {}
}
```

### Loupe Facet

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

import { LibDiamond } from "./LibDiamond.sol";
import { IDiamondLoupe } from "./IDiamondLoupe.sol";
import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol";

contract DiamondLoupeFacet is IDiamondLoupe, IERC165 {
    function facets() external view override returns (Facet[] memory facets_) {
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        uint256 numFacets = ds.facetAddresses.length;
        facets_ = new Facet[](numFacets);
        for (uint256 i; i < numFacets; i++) {
            address facetAddress_ = ds.facetAddresses[i];
            facets_[i].facetAddress = facetAddress_;
            facets_[i].functionSelectors = ds.facetFunctionSelectors[facetAddress_].functionSelectors;
        }
    }

    function facetFunctionSelectors(address _facet) external view override returns (bytes4[] memory facetFunctionSelectors_) {
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        facetFunctionSelectors_ = ds.facetFunctionSelectors[_facet].functionSelectors;
    }

    function facetAddresses() external view override returns (address[] memory facetAddresses_) {
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        facetAddresses_ = ds.facetAddresses;
    }

    function facetAddress(bytes4 _functionSelector) external view override returns (address facetAddress_) {
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        facetAddress_ = ds.selectorToFacetAndPosition[_functionSelector].facetAddress;
    }

    function supportsInterface(bytes4 _interfaceId) external view override returns (bool) {
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        return ds.supportedInterfaces[_interfaceId];
    }
}
```

---

## 2. Metamorphic Contracts (Pre-Dencun)

Metamorphic contracts exploit CREATE2's deterministic addressing combined with selfdestruct to deploy different bytecode at the same address. **Post-EIP-6780 (Dencun, March 2024)**, selfdestruct only sends ETH; it no longer deletes code unless the contract was created in the same transaction. This pattern is now effectively deprecated for new designs on mainnet.

### Pre-Dencun Pattern (Historical Reference)

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19; // Pre-Dencun semantics

// The factory deploys a transient "deployer" at a fixed salt,
// which immediately deploys the real implementation and selfdestructs.
// Net effect: implementation bytecode at the CREATE2 address can change.

contract MetamorphicFactory {
    // Immutable: the address where the metamorphic contract lives
    address public immutable metamorphicAddress;
    // Temporary storage for the initcode to be deployed
    bytes private _initCode;

    bytes32 constant SALT = bytes32(0);

    // The creation code of the "metamorphic" contract:
    // reads initcode from factory and deploys it via CREATE
    bytes constant METAMORPHIC_INIT =
        hex"5860208158601c335a63aaf10f428752fa158151803b80938091923cf3";

    constructor() {
        metamorphicAddress = address(
            uint160(uint256(keccak256(abi.encodePacked(
                hex"ff",
                address(this),
                SALT,
                keccak256(METAMORPHIC_INIT)
            ))))
        );
    }

    function deploy(bytes memory initCode) external returns (address deployed) {
        _initCode = initCode;
        bytes memory metamorphicInit = METAMORPHIC_INIT;
        assembly {
            deployed := create2(0, add(metamorphicInit, 32), mload(metamorphicInit), 0) // SALT = 0
        }
        require(deployed == metamorphicAddress, "MetamorphicFactory: wrong address");
        delete _initCode;
    }

    // Called by the transient deployer to retrieve initcode
    function getInitCode() external view returns (bytes memory) {
        return _initCode;
    }
}
```

### Post-Dencun Alternative: Just Use UUPS or Beacon

For any use case that previously relied on metamorphic contracts, prefer:
- UUPS proxy (single contract, self-upgradeable)
- Beacon proxy (upgrade many instances simultaneously)
- CREATE3 (deterministic address without redeployment)

---

## 3. Namespaced Storage (EIP-7201)

EIP-7201 standardizes where each logical module stores its state to prevent slot collisions across proxies and inherited contracts.

### Formula

```
storage_slot = keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff))
```

The `& ~0xff` operation zeros the last byte, reserving 256 slots for struct members starting at the computed slot (struct layout proceeds linearly from slot N).

### Reference Implementation

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

// EIP-7201: Namespaced Storage
// @custom:storage-location erc7201:myapp.storage.v1
contract NamespacedStorageExample {
    /// @custom:storage-location erc7201:myapp.storage.token
    struct TokenStorage {
        uint256 totalSupply;
        mapping(address => uint256) balances;
        mapping(address => mapping(address => uint256)) allowances;
        string name;
        string symbol;
    }

    // keccak256(abi.encode(uint256(keccak256("myapp.storage.token")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant TOKEN_STORAGE_LOCATION =
        0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00;

    function _getTokenStorage() private pure returns (TokenStorage storage $) {
        assembly {
            $.slot := TOKEN_STORAGE_LOCATION
        }
    }

    function totalSupply() public view returns (uint256) {
        return _getTokenStorage().totalSupply;
    }

    function balanceOf(address account) public view returns (uint256) {
        return _getTokenStorage().balances[account];
    }
}
```

### OpenZeppelin v5 Pattern

OZ v5 uses this exact pattern in all upgradeable contracts. Example from `OwnableUpgradeable`:

```solidity
// From OpenZeppelin Contracts v5
abstract contract OwnableUpgradeable is Initializable, OwnableStorage {
    // @custom:storage-location erc7201:openzeppelin.storage.Ownable
    struct OwnableStorage {
        address _owner;
    }

    bytes32 private constant OwnableStorageLocation =
        0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300;

    function _getOwnableStorage() private pure returns (OwnableStorage storage $) {
        assembly {
            $.slot := OwnableStorageLocation
        }
    }
}
```

### Migration from Unstructured Storage

When migrating from unstructured (append-at-fixed-slot) to namespaced storage:
1. Deploy a new implementation with EIP-7201 slots.
2. Write a migration initializer that copies values from old slots to new struct.
3. Use `reinitializer(2)` to guard it.
4. After migration, drop the old slot variables from layout (leave gaps or use `uint256[N] __gap`).

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

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

contract TokenV2 is Initializable {
    // Old unstructured slot (slot 0 in original layout) — must keep for migration
    // DO NOT remove until after migration is complete across all proxies
    uint256 private _legacyTotalSupply; // slot 0

    /// @custom:storage-location erc7201:myapp.storage.token
    struct TokenStorage {
        uint256 totalSupply;
        mapping(address => uint256) balances;
    }

    bytes32 private constant TOKEN_STORAGE_LOCATION =
        0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00;

    function _getTokenStorage() private pure returns (TokenStorage storage $) {
        assembly {
            $.slot := TOKEN_STORAGE_LOCATION
        }
    }

    function initializeV2() external reinitializer(2) {
        TokenStorage storage $ = _getTokenStorage();
        // Migrate from legacy slot 0
        $.totalSupply = _legacyTotalSupply;
        _legacyTotalSupply = 0; // Clear old slot
    }
}
```

---

## 4. Beacon Proxy Pattern

The Beacon pattern decouples the implementation address from individual proxies. All proxies point to a beacon; changing the beacon's implementation pointer upgrades all proxies in a single transaction.

### When to Use Beacon vs UUPS vs Transparent

| Criterion | Beacon | UUPS | Transparent |
|---|---|---|---|
| Mass upgrade (N instances) | Best — 1 tx | N txs | N txs |
| Upgrade authorization gas | Low (beacon storage) | In implementation | Admin slot |
| Individual upgrade possible | No (by design) | Yes | Yes |
| Logic separation | Clean | Mixed | Mixed |
| Deployment cost | Higher (beacon contract) | Lower | Higher (admin logic) |

### UpgradeableBeacon

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

import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";

// Deploy sequence:
// 1. Deploy implementation
// 2. Deploy UpgradeableBeacon(implementation, owner)
// 3. For each instance: deploy BeaconProxy(beacon, initData)

contract VaultImplementation {
    address private _beacon; // not used in impl, only in proxy context

    /// @custom:storage-location erc7201:myapp.storage.vault
    struct VaultStorage {
        address owner;
        uint256 balance;
        bool initialized;
    }

    bytes32 private constant VAULT_STORAGE_LOCATION =
        0xabcd000000000000000000000000000000000000000000000000000000000000; // use real hash

    function _getVaultStorage() private pure returns (VaultStorage storage $) {
        assembly {
            $.slot := VAULT_STORAGE_LOCATION
        }
    }

    function initialize(address owner) external {
        VaultStorage storage $ = _getVaultStorage();
        require(!$.initialized, "Already initialized");
        $.initialized = true;
        $.owner = owner;
    }

    function deposit() external payable {
        _getVaultStorage().balance += msg.value;
    }
}
```

### Deployment Script (Foundry)

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

import "forge-std/Script.sol";
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import "./VaultImplementation.sol";

contract DeployBeaconSystem is Script {
    function run() external {
        uint256 deployerKey = vm.envUint("PRIVATE_KEY");
        address deployer = vm.addr(deployerKey);

        vm.startBroadcast(deployerKey);

        // 1. Deploy implementation
        VaultImplementation impl = new VaultImplementation();

        // 2. Deploy beacon (owner = deployer, can be transferred to multisig)
        UpgradeableBeacon beacon = new UpgradeableBeacon(address(impl), deployer);

        // 3. Deploy proxy instances
        for (uint256 i = 0; i < 3; i++) {
            bytes memory initData = abi.encodeCall(VaultImplementation.initialize, (deployer));
            BeaconProxy proxy = new BeaconProxy(address(beacon), initData);
            console.log("Vault proxy", i, ":", address(proxy));
        }

        vm.stopBroadcast();

        console.log("Implementation:", address(impl));
        console.log("Beacon:", address(beacon));
    }

    function upgrade(address beacon, address newImpl) external {
        uint256 deployerKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerKey);

        VaultImplementation newImplContract = new VaultImplementation();
        UpgradeableBeacon(beacon).upgradeTo(address(newImplContract));

        vm.stopBroadcast();
        console.log("Upgraded beacon to:", address(newImplContract));
    }
}
```

---

## 5. UUPS vs Transparent Proxy

### Transparent Proxy

The admin address is stored in a dedicated EIP-1967 slot (`keccak256("eip1967.proxy.admin") - 1`). The proxy intercepts all admin calls (upgrade, changeAdmin) and forwards all other calls to the implementation. The admin can never call implementation functions directly.

**Storage layout**: Implementation is stored at `keccak256("eip1967.proxy.implementation") - 1`.

```
EIP-1967 slots:
  implementation: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
  admin:          0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
  beacon:         0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
```

### UUPS Proxy

The upgrade logic lives in the implementation, not the proxy. The proxy is minimal — just a delegatecall forwarder. `UUPSUpgradeable` in OpenZeppelin provides the `upgradeTo` / `upgradeToAndCall` functions with an `_authorizeUpgrade` hook.

**Security risk**: If the implementation is bricked (broken `_authorizeUpgrade`), the proxy is permanently locked. Never deploy UUPS without a multisig or timelock holding upgrade authority.

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

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

contract MyProtocolV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    /// @custom:storage-location erc7201:myprotocol.storage.v1
    struct ProtocolStorage {
        uint256 version;
        mapping(address => uint256) stakes;
        uint256 totalStaked;
    }

    bytes32 private constant PROTOCOL_STORAGE_LOCATION =
        0xf00d000000000000000000000000000000000000000000000000000000000000; // real hash required

    function _getProtocolStorage() private pure returns (ProtocolStorage storage $) {
        assembly {
            $.slot := PROTOCOL_STORAGE_LOCATION
        }
    }

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

    function initialize(address initialOwner) external initializer {
        __Ownable_init(initialOwner);
        __UUPSUpgradeable_init();
        _getProtocolStorage().version = 1;
    }

    // Only owner can authorize upgrades. In production: owner = timelock.
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}

    function stake() external payable {
        ProtocolStorage storage $ = _getProtocolStorage();
        $.stakes[msg.sender] += msg.value;
        $.totalStaked += msg.value;
    }

    function version() external view returns (uint256) {
        return _getProtocolStorage().version;
    }
}
```

### Storage Collision Risk

The classic collision: if an implementation uses slot 0 for a variable AND the proxy stores the implementation address at slot 0, delegatecall will corrupt both. EIP-1967 solves this with pseudo-random slots. With namespaced storage (EIP-7201), collision risk is negligible — each module hashes a unique string.

**Never** inherit from non-upgradeable contracts in an upgradeable implementation. `Ownable` vs `OwnableUpgradeable` is a classic footgun: `Ownable` uses slot 0 for `_owner`, which will collide with implementation storage in a pre-7201 layout.

---

## 6. Minimal Proxy (EIP-1167)

EIP-1167 defines a 45-byte bytecode template that delegatecalls to a fixed implementation. Used for cheap "clone" deployments — ~10x cheaper than full deployment.

### Clone Bytecode (45 bytes)

```
363d3d373d3d3d363d73<20-byte-address>5af43d82803e903d91602b57fd5bf3
```

### OpenZeppelin Clones

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

import "@openzeppelin/contracts/proxy/Clones.sol";

contract CloneFactory {
    using Clones for address;

    address public immutable implementation;
    address[] public clones;

    event CloneDeployed(address indexed clone, address indexed owner);

    constructor(address _implementation) {
        implementation = _implementation;
    }

    // Standard clone: cheapest, same address pattern each time
    function createClone(address owner) external returns (address clone) {
        clone = implementation.clone();
        IInitializable(clone).initialize(owner);
        clones.push(clone);
        emit CloneDeployed(clone, owner);
    }

    // Deterministic clone: same address for same salt across any chain
    function createDeterministicClone(address owner, bytes32 salt) external returns (address clone) {
        clone = implementation.cloneDeterministic(salt);
        IInitializable(clone).initialize(owner);
        clones.push(clone);
        emit CloneDeployed(clone, owner);
    }

    // Predict address before deployment
    function predictCloneAddress(bytes32 salt) external view returns (address) {
        return implementation.predictDeterministicAddress(salt);
    }
}

interface IInitializable {
    function initialize(address owner) external;
}
```

### Clones With Immutable Args (CWIA)

CWIA appends immutable arguments to the clone bytecode. These are read via `calldata` slicing in the implementation, avoiding storage costs.

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

// Using wightstufff/clones-with-immutable-args
import {ClonesWithImmutableArgs} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol";
import {Clone} from "clones-with-immutable-args/Clone.sol";

contract VaultCWIA is Clone {
    // Read immutable args baked into bytecode at deploy time
    function owner() public pure returns (address) {
        return _getArgAddress(0); // first 20 bytes of appended args
    }

    function maxDeposit() public pure returns (uint256) {
        return _getArgUint256(20); // next 32 bytes
    }

    function deposit() external payable {
        require(msg.value <= maxDeposit(), "Exceeds max");
    }
}

contract VaultCWIAFactory {
    using ClonesWithImmutableArgs for address;

    address public immutable implementation;

    constructor(address _implementation) {
        implementation = _implementation;
    }

    function createVault(address vaultOwner, uint256 maxDep) external returns (address vault) {
        bytes memory data = abi.encodePacked(vaultOwner, maxDep);
        vault = implementation.clone(data);
        // No initialize() needed — owner and maxDeposit are in bytecode
    }
}
```

---

## 7. Storage Layout Compatibility

### The Append-Only Rule

When upgrading an implementation:
- **Never remove** storage variables.
- **Never reorder** storage variables.
- **Never change the type** of a variable (even same-size types like `uint256` → `int256` can change semantics).
- **Always append** new variables at the end.
- **Mappings are safe** to "expand" conceptually — mapping slots are computed by key hash, not sequential.

### Gap Pattern

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

abstract contract StorageV1 {
    uint256 public value;
    address public admin;
    // Reserve 48 slots for future use without breaking layout
    uint256[48] private __gap;
}

// Safe upgrade: use gap slots
abstract contract StorageV2 is StorageV1 {
    // "Consume" one gap slot by adding a new variable
    // __gap shrinks by 1 implicitly — but this ONLY works if we
    // explicitly override __gap to length 47 in the child
    uint256 public newValue;
    uint256[47] private __gap; // Was 48, now 47 — net slot count unchanged
}
```

**Preferred modern alternative**: Use EIP-7201 namespaced storage structs. Adding fields to the end of a struct is safe (struct members are laid out sequentially from the struct's base slot). Gap patterns become unnecessary.

### Layout Verification with Foundry

```bash
# Check storage layout of a contract
forge inspect MyContractV2 storage-layout

# Compare layouts between versions (pipe to jq for diff)
forge inspect MyContractV1 storage-layout --json > v1.json
forge inspect MyContractV2 storage-layout --json > v2.json
diff v1.json v2.json
```

---

## 8. Upgrade Safety Checks

### OpenZeppelin Upgrades Plugin (Hardhat / Foundry)

The plugin statically analyzes storage layout and flags incompatible changes before deployment.

```typescript
// hardhat.config.ts
import "@openzeppelin/hardhat-upgrades";

// deploy/001_deploy_proxy.ts
import { ethers, upgrades } from "hardhat";

async function main() {
    const MyProtocol = await ethers.getContractFactory("MyProtocolV1");

    // deployProxy validates the contract is upgrade-safe
    const proxy = await upgrades.deployProxy(MyProtocol, [await ethers.getSigners()[0].getAddress()], {
        kind: "uups",
        initializer: "initialize",
    });
    await proxy.waitForDeployment();
    console.log("Proxy:", await proxy.getAddress());

    // Upgrade (validates storage layout compatibility)
    const MyProtocolV2 = await ethers.getContractFactory("MyProtocolV2");
    const upgraded = await upgrades.upgradeProxy(await proxy.getAddress(), MyProtocolV2, {
        kind: "uups",
        call: { fn: "initializeV2", args: [] }, // optional reinitializer call
    });
    console.log("Upgraded to V2");
}
```

### Foundry with OZ Upgrades

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

import "forge-std/Test.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";

contract UpgradeTest is Test {
    address proxy;

    function setUp() public {
        proxy = Upgrades.deployUUPSProxy(
            "MyProtocolV1.sol",
            abi.encodeCall(MyProtocolV1.initialize, (address(this)))
        );
    }

    function testUpgrade() public {
        // This validates storage layout compatibility at test time
        Upgrades.upgradeProxy(
            proxy,
            "MyProtocolV2.sol",
            abi.encodeCall(MyProtocolV2.initializeV2, ())
        );
        MyProtocolV2 v2 = MyProtocolV2(proxy);
        assertEq(v2.version(), 2);
    }
}
```

### Annotation-Based Safety

```solidity
// Mark constructor as safe to skip initializer check
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
    _disableInitializers();
}

// Mark a state variable as intentionally uninitialized
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address private immutable _factory;

// Allow delegatecall in a specific function
/// @custom:oz-upgrades-unsafe-allow delegatecall
function exec(address target, bytes calldata data) external {
    (bool ok,) = target.delegatecall(data);
    require(ok);
}
```

---

## 9. Multi-Implementation Proxies (Routing Proxies)

A routing proxy dispatches calls to different implementations based on a registry. Unlike Diamond (function-selector routing), routing proxies can route by caller, version tag, or feature flag.

### Version Registry Proxy

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

contract VersionRegistry {
    address public owner;
    mapping(bytes32 => address) public implementations;
    bytes32 public currentVersion;

    event VersionRegistered(bytes32 indexed version, address implementation);
    event VersionActivated(bytes32 indexed version);

    constructor(address _owner) {
        owner = _owner;
    }

    function registerVersion(bytes32 versionTag, address implementation) external {
        require(msg.sender == owner, "Not owner");
        require(implementation.code.length > 0, "No code");
        implementations[versionTag] = implementation;
        emit VersionRegistered(versionTag, implementation);
    }

    function activateVersion(bytes32 versionTag) external {
        require(msg.sender == owner, "Not owner");
        require(implementations[versionTag] != address(0), "Version not registered");
        currentVersion = versionTag;
        emit VersionActivated(versionTag);
    }

    function currentImplementation() external view returns (address) {
        return implementations[currentVersion];
    }
}

contract RoutingProxy {
    VersionRegistry public immutable registry;

    constructor(address _registry) {
        registry = VersionRegistry(_registry);
    }

    fallback() external payable {
        address impl = registry.currentImplementation();
        require(impl != address(0), "No active 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()) }
        }
    }

    receive() external payable {}
}
```

### Per-Caller Implementation Routing

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

// Useful for gradual rollouts: some callers get V2, others stay on V1
contract GradualRolloutProxy {
    address public admin;
    address public defaultImpl;
    mapping(address => address) public callerOverrides;

    constructor(address _admin, address _defaultImpl) {
        admin = _admin;
        defaultImpl = _defaultImpl;
    }

    function setCallerOverride(address caller, address impl) external {
        require(msg.sender == admin);
        callerOverrides[caller] = impl;
    }

    function setDefaultImpl(address impl) external {
        require(msg.sender == admin);
        defaultImpl = impl;
    }

    fallback() external payable {
        address impl = callerOverrides[msg.sender];
        if (impl == address(0)) impl = defaultImpl;
        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()) }
        }
    }

    receive() external payable {}
}
```

---

## 10. Initializer Patterns

### `initializer` vs `reinitializer`

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

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

contract MultiVersionContract is Initializable {
    uint256 public x;
    uint256 public y;
    uint256 public z;

    // Called once at V1 deployment
    function initialize(uint256 _x) external initializer {
        x = _x;
    }

    // Called once at V2 upgrade — guards against re-execution
    // reinitializer(N) allows calling if _initialized < N
    function initializeV2(uint256 _y) external reinitializer(2) {
        y = _y;
    }

    // V3 upgrade
    function initializeV3(uint256 _z) external reinitializ

…(truncated)
