Writing Upgradable Smart Contracts

 1) Why upgrades?


Bug fixes & security patches


Feature evolution without migrating state


Parameter tuning (fees, limits) via governance


2) Core idea: Proxy + Logic


Users interact with a proxy that holds state and delegatecalls to an implementation (logic) contract you can replace.


Popular patterns


Transparent Proxy (EIP-1967)


Separate ProxyAdmin account controls upgrades.


Calls from admin go to admin functions; all others are delegated.


Simple, battle-tested; default in OpenZeppelin.


UUPS (EIP-1822)


The implementation contract contains the upgrade function (upgradeTo).


Proxy is minimal; saves gas on deployments.


Requires an onlyProxy guard and proxiableUUID safety.


Beacon Proxy


Many proxies point to a Beacon that holds the implementation address.


Upgrading the beacon upgrades all proxies at once (good for factories).


Diamond (EIP-2535)


Modular “facets” with function selectors; advanced/composable.


Powerful but complex; only use if you really need modular hot-swapping.


3) Storage layout rules (super important)


Upgrades must not corrupt storage. Follow these rules:


Never change the order of existing state variables.


Do not remove or rename existing variables.


Only append new variables at the end.


Reserve space using uint256[50] private __gap; for future variables (OZ pattern).


Avoid immutable and constructor storage (use initializers instead).


Be careful when changing inheritance—it affects layout.


Don’t switch between 721/1155 mixins or libraries that alter storage slots without a plan.


4) Constructors vs Initializers


Proxies don’t run constructors of the implementation. Use initializer functions.


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;


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


contract ExampleV1 is Initializable {

    uint256 public x;

    address public owner;

    uint256[48] private __gap; // keep room for future vars


    function initialize(uint256 _x, address _owner) public initializer {

        x = _x;

        owner = _owner;

    }

}



Use initializer for the first init; for later versions use reinitializer(version) (e.g., reinitializer(2)).


Disable initializers in the implementation’s constructor: _disableInitializers(); to prevent someone from initializing the logic contract directly.


5) UUPS upgradeable example

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;


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

import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

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


contract CounterV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {

    uint256 public count;

    uint256[49] private __gap; // reserve


    function initialize(uint256 _start) public initializer {

        __Ownable_init(msg.sender);

        __UUPSUpgradeable_init();

        count = _start;

    }


    function inc() external { count += 1; }


    // UUPS authorization

    function _authorizeUpgrade(address newImpl) internal override onlyOwner {}

}


// V2 keeps SAME storage layout + appends only

contract CounterV2 is CounterV1 {

    function dec() external { require(count > 0, "underflow"); count -= 1; }

}


6) Transparent proxy example (concept)


Deploy CounterV1.


Deploy TransparentUpgradeableProxy with:


implementation = CounterV1


admin = ProxyAdmin


data = encoded initialize(...)


To upgrade:


Deploy CounterV2.


ProxyAdmin.upgrade(proxy, implV2).


7) Access control & governance


Upgrades should be restricted by:


ProxyAdmin owner (EOA or multisig) for Transparent pattern.


_authorizeUpgrade (UUPS) – typically Ownable, RBAC, or DAO timelock.


Best practice: Multisig + Timelock (e.g., 24–48h) governed by a DAO. This lets users act if a malicious upgrade is queued.


Emit Upgrade events; publish diffs & audits for transparency.


8) What you can change safely


Add new functions.


Add new state variables at the end.


Fix logic bugs that don’t rely on re-ordered storage.


Introduce new modules via inheritance after existing storage layout.


What you must not do:


Reorder or delete variables.


Change types/sizes of existing variables.


Move a parent contract with storage ahead of another parent in inheritance linearization.


Switch to libraries that introduce new storage slots at the top.


9) Tooling (recommended)


OpenZeppelin Upgrades Plugins


Hardhat: @openzeppelin/hardhat-upgrades


Foundry: openzeppelin-foundry-upgrades


Truffle: @openzeppelin/truffle-upgrades


These:


Deploy proxies, run initializers.


Validate storage layout across versions.


Protect UUPS with proxiableUUID checks.


Provide upgradeProxy helpers.


Hardhat example


// deploy.js

const { ethers, upgrades } = require("hardhat");


async function main() {

  const CounterV1 = await ethers.getContractFactory("CounterV1");

  const proxy = await upgrades.deployProxy(CounterV1, [5], { kind: "uups" });

  await proxy.waitForDeployment();

  console.log("Proxy at:", await proxy.getAddress());

}


// upgrade.js

const { ethers, upgrades } = require("hardhat");

async function main() {

  const CounterV2 = await ethers.getContractFactory("CounterV2");

  const proxyAddr = "0xYourProxy";

  await upgrades.upgradeProxy(proxyAddr, CounterV2); // validates layout

}


10) Testing upgrades (must-do)


Unit tests for each version (V1, V2…).


Storage layout test: set state in V1 → upgrade → assert values unchanged in V2.


Event & behavior tests: ensure new functions work and old functions behave identically.


Fuzz/Property tests: check invariants across upgrade boundaries.


Simulate governance: queue → timelock → execute → upgrade.


11) Security checklist


Use ReentrancyGuard where needed; respect checks-effects-interactions.


Add Pausable (circuit breaker) in case of emergencies.


Disable initializers in implementation constructors.


Protect UUPS _authorizeUpgrade.


For Transparent proxies, keep ProxyAdmin on a multisig.


Consider upgrade beacons for mass upgrades; beware blast radius.


Avoid selfdestruct (deprecated and dangerous for upgrade assumptions).


Document & verify EIP-1967 slots if writing custom proxies.


Run static analysis (Slither), linters, and use audited libraries (OpenZeppelin).


12) Versioning & operations


Tag each implementation with semver (v1.2.0), publish ABI & commit hash.


Maintain a changelog with storage diffs and migration notes.


Roll out upgrades with canary deployments (one instance or small subset first, if your architecture allows).


Keep runbooks for rollback (upgrade back to previous impl).


13) Common pitfalls (and fixes)


Constructor logic lost → move to initialize/reinitializer.


Storage collision → use OZ plugins to validate; append only; keep __gap.


Calling upgrade functions on the implementation → block by _disableInitializers and access control.


Delegatecall context gotchas → address(this) is the proxy; msg.sender is the external caller.


Immutable variables → they won’t be changeable after upgrade; consider regular storage instead.


Library storage → ensure libs don’t introduce unexpected slots, or use library with pure/view logic.


14) When to choose which pattern?


Transparent Proxy: simplest ops, clear separation—great default for most apps.


UUPS: you want minimal proxies, lots of instances, and are comfortable enforcing _authorizeUpgrade.


Beacon: factory pattern with many identical clones that should upgrade together.


Diamond: complex, large codebase with feature modules and selective upgrades.


Quick upgrade playbook


Implement V1 with initializer, __gap, access control, and events.


Deploy via OZ Upgrades (Transparent or UUPS).


Write tests that persist state, then upgrade to V2 in tests.


Add new vars at the end; never reorder.


Gate upgrades with multisig + timelock, announce, and monitor.


If you want, I can:


convert this into a step-by-step checklist for your repo,


provide a ready-to-run Hardhat/Foundry template,


or adapt the examples for Transparent or Beacon patterns with full scripts.

Learn Blockchain Course in Hyderabad

Read More

Building an NFT Marketplace from Scratch

Flash Loan Arbitrage: Real-World Examples

Flash Loan Arbitrage: Real-World Examples

Time Locks and Vesting in Smart Contracts

Comments

Popular posts from this blog

Handling Frames and Iframes Using Playwright

Cybersecurity Internship Opportunities in Hyderabad for Freshers

Tosca for API Testing: A Step-by-Step Tutorial