🏠 Home
Beginner
01 — Introduction to Solidity 02 — Setting Up Your Environment 03 — Your First Smart Contract 04 — Data Types & Variables 05 — Functions & Visibility 06 — Control Flow 07 — Arrays & Mappings 08 — Structs & Enums
Intermediate
09 — Events & Logging 10 — Modifiers 11 — Inheritance 12 — Interfaces & Abstract Contracts 13 — Error Handling 14 — Ether & Wei 15 — Payable Functions 16 — msg.sender & msg.value 17 — Storage vs Memory vs Stack
Advanced
18 — Gas Optimization 19 — ERC-20 Tokens 20 — ERC-721 NFT Standard 21 — Contract Security 22 — Reentrancy Attacks 23 — Oracles & Chainlink 24 — Upgradeable Contracts 25 — Deploying to Mainnet
SolidityMaster / Lesson 24
Lesson 24 of 25

Upgradeable Contracts

Proxy patterns (UUPS, Transparent) to make your contracts upgradeable post-deploy.

Advanced

Why Proxies?

Deployed Solidity code is immutable. To upgrade a contract's logic, use a proxy pattern:
  1. Users interact with a Proxy contract (permanent address, just stores state)
  2. The proxy delegatecalls to an Implementation contract (upgradeable logic)
  3. To upgrade: deploy new implementation, point proxy to it
Solidity
// How delegatecall works:
// When proxy delegatecalls to implementation:
// - Implementation's CODE runs...
// - ...but in the PROXY's storage context
// - msg.sender and msg.value are preserved

// Minimal proxy (EIP-1167 Clone Factory)
contract MinimalProxy {
    address public implementation;

    constructor(address _impl) { implementation = _impl; }

    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()) }
        }
    }
}

UUPS Proxy Pattern (Recommended)

UUPS (Universal Upgradeable Proxy Standard — EIP-1822) puts the upgrade logic IN the implementation, reducing proxy gas overhead. Use OpenZeppelin's battle-tested implementation.
Solidity
// Implementation contract (with upgrade logic)
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

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

    // Use initialize() instead of constructor
    function initialize(uint256 _value) public initializer {
        __Ownable_init(msg.sender);
        __UUPSUpgradeable_init();
        value = _value;
    }

    function setValue(uint256 _value) public onlyOwner {
        value = _value;
    }

    // Only owner can upgrade
    function _authorizeUpgrade(address) internal override onlyOwner {}
}

// Deploy script (Hardhat + OpenZeppelin Upgrades):
// const proxy = await upgrades.deployProxy(MyContractV1, [42]);
// const upgraded = await upgrades.upgradeProxy(proxy, MyContractV2);

Storage Layout Rules

When upgrading, you MUST NOT change the order or type of existing storage variables. Only add new variables at the END.
Solidity
// V1 storage layout
contract V1 {
    uint256 public a; // slot 0
    address public b; // slot 1
}

// ✅ V2 — safe upgrade, only adds new slot
contract V2 {
    uint256 public a; // slot 0 — unchanged
    address public b; // slot 1 — unchanged
    uint256 public c; // slot 2 — NEW — safe to add
}

// ❌ V2 — BROKEN — reorders slots, corrupts storage
contract V2Broken {
    address public b; // slot 0 — was uint256!
    uint256 public a; // slot 1 — was address!
}