Intermediate

Proxies & Upgradeable Contracts — Kudos build — upgrade path

Understand how upgrades work.

Developer Intermediate
5/6 — Proxies & Upgradeable Contracts — Kudos build — upgrade path

Objective Put the Kudos token/badge behind UUPS proxies so we can ship new features without changing addresses.

Flow Create upgradeable versions, guard _authorizeUpgrade, expose a version getter, and rehearse an upgrade script.

What to write

  1. Upgradeable scaffold
    Initialize roles, token, and permit.
    contract KudosUpgradeable is Initializable, UUPSUpgradeable, AccessControlUpgradeable, ERC20Upgradeable, ERC20PermitUpgradeable {
        bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
    
        function initialize() public initializer {
            __ERC20_init("Kudos", "KUDOS");
            __ERC20Permit_init("Kudos");
            __AccessControl_init();
            __UUPSUpgradeable_init();
            _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
            _grantRole(UPGRADER_ROLE, msg.sender);
            _mint(msg.sender, 1_000_000 ether);
        }

    Initializers replace constructors behind proxies; roles stay intact.

  2. Guard upgrades
    Restrict _authorizeUpgrade and expose version.
        function _authorizeUpgrade(address) internal override onlyRole(UPGRADER_ROLE) {}
        function version() external pure returns (string memory) { return "v2"; }
    }

    Locks upgrades to a role and exposes versioning for the UI.

  3. Deploy script
    Proxy deploy + initialize.
    // Hardhat example
    const impl = await ethers.deployContract("KudosUpgradeable");
    const proxy = await upgrades.deployProxy(impl, [], { initializer: "initialize" });
    console.log("proxy", await proxy.getAddress());

    Ensures initialize runs once and addresses stay stable; verify implementation/admin slots after deploy.

Why Proxies keep addresses stable, version() gives visibility, and guarded upgrades plus rehearsals prevent bricking user balances.

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

import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import {AccessControlUpgradeable} from "@openzeppelin/contracts/access/AccessControlUpgradeable.sol";
import {ERC20Upgradeable} from "@openzeppelin/contracts/token/ERC20/ERC20Upgradeable.sol";
import {ERC20PermitUpgradeable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol";

contract KudosUpgradeable is Initializable, UUPSUpgradeable, AccessControlUpgradeable, ERC20Upgradeable, ERC20PermitUpgradeable {
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    function initialize() public initializer {
        __ERC20_init("Kudos", "KUDOS");
        __ERC20Permit_init("Kudos");
        __AccessControl_init();
        __UUPSUpgradeable_init();
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(UPGRADER_ROLE, msg.sender);
        _mint(msg.sender, 1_000_000 ether);
    }

    function _authorizeUpgrade(address) internal override onlyRole(UPGRADER_ROLE) {}
    function version() external pure returns (string memory) { return "v2"; }
}

Next Lock this stack down with thorough tests.

← Previous section
Next section →