iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Upgradeability

Smart contract upgrades: proxy patterns, OpenZeppelin Upgradeable, and the trade-offs between immutability and iteration.

Web3 — contract upgrades

EXAMPLE
// ===== Why upgrade =====
// Contracts ship with bugs. Immutable contracts mean bugs are forever.
// Upgrade patterns let you fix logic while preserving state.
// Cost: trust assumptions (proxy owner) + complexity.

// ===== The proxy pattern =====
// User -> Proxy contract -> delegatecall -> Implementation contract
// Proxy stores STATE; implementation has LOGIC.
// Upgrade by pointing the proxy at a new implementation.

// ===== Transparent vs UUPS =====
// Transparent Proxy (older): proxy owner cannot call regular functions; admins go through ProxyAdmin.
// UUPS (newer, recommended): upgrade logic lives in the IMPLEMENTATION; smaller proxy bytecode.

// ===== UUPS example (OpenZeppelin Upgradeable) =====
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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 counter;

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

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

    function increment() public { counter += 1; }

    function _authorizeUpgrade(address newImpl) internal override onlyOwner {}
}

// ===== Deploy via Hardhat (OpenZeppelin upgrades plugin) =====
// npm install @openzeppelin/hardhat-upgrades

// scripts/deploy.js
const { ethers, upgrades } = require('hardhat');
async function main() {
  const MyContract = await ethers.getContractFactory('MyContractV1');
  const proxy = await upgrades.deployProxy(MyContract, [deployerAddress], { kind: 'uups' });
  await proxy.waitForDeployment();
  console.log('Proxy:', await proxy.getAddress());
}

// Upgrade:
const MyContractV2 = await ethers.getContractFactory('MyContractV2');
await upgrades.upgradeProxy(proxyAddress, MyContractV2);

// ===== Storage layout rules =====
// You CANNOT reorder or remove storage variables in the new implementation.
// Always APPEND new variables at the end.
// OpenZeppelin tools verify this.

// Bad:
// V1: uint256 counter; uint256 totalSupply;
// V2: uint256 totalSupply; uint256 counter;   // storage corrupted

// Good:
// V1: uint256 counter; uint256 totalSupply;
// V2: uint256 counter; uint256 totalSupply; address newField;   // append only

// ===== Timelocks =====
// Add a TIMELOCK between proposing and executing an upgrade.
// Users have time to exit if they disagree.
// OpenZeppelin TimelockController is standard.

// ===== Diamond pattern (advanced) =====
// EIP-2535: single proxy + many 'facets' for modular contracts.
// More flexible but more complex.

// ===== When NOT to upgrade =====
// - Token contracts where immutability is the value proposition (some DeFi)
// - Contracts holding very high TVL where trust is critical
// - Anything where users explicitly want 'no admin'

// Alternative: deploy v2 as a NEW contract; users migrate voluntarily.

// ===== Patterns to internalise =====
// - UUPS over Transparent for new contracts
// - Storage layout: APPEND only, never reorder
// - Timelock + multisig on upgrade authorisation
// - Test upgrades on fork before mainnet

// ===== Pitfalls =====
// - Constructor in upgradeable contracts -> use initialize()
// - Reordering storage -> total state corruption
// - Selfdestruct in implementation -> proxy bricked
// - Forgetting _disableInitializers in the implementation constructor

Why it matters

Smart contract upgrades trade immutability for iteration via proxy patterns. UUPS is the modern default; OpenZeppelin Upgradeable + Hardhat plugin manage the boilerplate. Storage layout discipline is non-negotiable. Pair with a timelock + multisig on upgrade authorisation, and test on a fork before mainnet.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Three common paths:
//   - Proxy + UUPS / Transparent (OpenZeppelin)
//   - Diamond (EIP-2535)
//   - Don't upgrade; redeploy + migrate.
// Each option trades flexibility for risk surface.
Try it Yourself »

Discussion

Loading…