2 Min Read

Introduction to Upgradable Smart Contracts

Smart contracts on Ethereum are immutable once deployed, but what if you need to fix bugs or add features post-launch? Enter upgradable smart contracts using proxy patterns. The UUPS (Universal Upgradeable Proxy Standard) pattern from OpenZeppelin is a secure, gas-efficient way to achieve this. In this 2026 guide, we'll build a secure implementation emphasizing initializer guards, storage gaps, and pitfalls like delegatecall risks.

UUPS proxies delegate calls to an implementation contract while storing data separately. Upgrades happen by updating the implementation address. This tutorial uses Solidity 0.8.26+, OpenZeppelin Contracts 5.x, Foundry for testing, and targets Ethereum L2s like Optimism and Arbitrum for production.

Why Choose UUPS Over Transparent Proxy?

  • Gas Efficiency: UUPS only checks admin permissions during upgrades, not every call.
  • Storage Safety: Built-in initializer modifiers prevent reinitialization attacks.
  • Flexibility: Implementation can be fully upgraded, including proxy logic.

Compared to Transparent proxies, UUPS avoids fallback function overhead. Check OpenZeppelin's official docs for deeper comparisons at docs.openzeppelin.com.

Project Setup

  1. Install Foundry: curl -L https://foundry.paradigm.xyz | bash && foundryup.
  2. Create a new Foundry project: forge init upgradable-contracts && cd upgradable-contracts.
  3. Add dependencies: forge install OpenZeppelin/openzeppelin-contracts@v5.0.2 and update remappings.txt.

Implementing the Logic (Implementation) Contract

Start with a simple counter contract that we'll make upgradable.

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

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

contract CounterV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    uint256 public count;

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

    function initialize(uint256 _initialCount) public initializer {
        __Ownable_init(msg.sender);
        __UUPSUpgradeable_init();
        count = _initialCount;
    }

    function increment() public {
        count++;
    }

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

Key security features:

  • Initializable: Uses initializer modifier to run setup once.
  • UUPSUpgradeable: Handles proxy delegation and upgrade authorization.
  • Constructor Disable: Calls _disableInitializers() to block direct initialization.

Deploying the UUPS Proxy

In Foundry scripts, deploy proxy and implementation separately.

// script/Deploy.s.sol
import {Script, console} from "forge-std/Script.sol";
import {CounterV1} from "../src/CounterV1.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract Deploy is Script {
    function run() external {
        vm.startBroadcast();
        CounterV1 impl = new CounterV1();
        ERC1967Proxy proxy = new ERC1967Proxy(address(impl), abi.encodeCall(CounterV1.initialize, (0)));
        console.log("Proxy deployed at:", address(proxy));
        vm.stopBroadcast();
    }
}

Run: forge script script/Deploy.s.sol --rpc-url $OPTIMISM_RPC_URL --broadcast.

Security Essentials: Initializer Guards and Storage Gaps

Initializer Guards

Prevent reinitialization attacks by inheriting Initializable and using initializer. For multiple inheritance, order matters—most-derived first.

Storage Gaps

OpenZeppelin libraries append variables after a storage gap (e.g., uint256[50] private __gap;). This reserves slots for future upgrades, preventing collisions.

Add to your contract:

uint256[50] private __gap;

Common Pitfalls and Delegatecall Risks

Delegatecall copies code to the proxy's context, using its storage. Risks include:

  • Storage Collisions: New implementations overwriting proxy storage—mitigated by gaps.
  • Self-Destruct: Never allow in implementation; it destroys proxy data.
  • Unauthorized Upgrades: _authorizeUpgrade must be onlyOwner or multisig.
  • Delegatecall Loops: Avoid recursive calls.

Avoid raw delegatecalls; use OpenZeppelin's audited proxies. For Ethereum upgrade docs, see ethereum.org.

Testing with Foundry

Write comprehensive tests for upgrades.

// test/Counter.t.sol
import {Test, console} from "forge-std/Test.sol";
// ... imports

contract CounterTest is Test {
    CounterV1 public proxy;
    CounterV1 public implV1;

    function setUp() public {
        implV1 = new CounterV1();
        bytes memory initData = abi.encodeCall(CounterV1.initialize, (0));
        ERC1967Proxy p = new ERC1967Proxy(address(implV1), initData);
        proxy = CounterV1(address(p));
    }

    function testIncrement() public {
        proxy.increment();
        assertEq(proxy.count(), 1);
    }

    function testUpgrade() public {
        // Deploy V2 with new function
        CounterV2 implV2 = new CounterV2();
        address payable proxyAddr = payable(address(proxy));
        vm.startPrank(proxy.owner());
        UUPSUpgradeable(payable(address(proxy))).upgradeToAndCall(address(implV2), "");
        vm.stopPrank();
        // Test V2 functionality
    }
}

Run: forge test --match-test testUpgrade. Foundry's fuzzing catches edge cases.

Deployment to Ethereum L2s

L2s like Optimism reduce costs. Use their RPCs:

  • Optimism: $OP_RPC_URL
  • Arbitrum: $ARB_RPC_URL

Verify on L2 explorers. Use multisig wallets (e.g., Safe) for onlyOwner.

Auditing Tips for 2026 Deployments

  1. Static Analysis: Slither, Mythril—slither ..
  2. Formal Verification: Certora for upgrade invariants.
  3. Manual Review: Check initializer order, gaps, no selfdestruct.
  4. Proxy-Specific: Test upgrade paths with fuzzing.
  5. Post-Audit: Bug bounties on Immunefi.

For Foundry best practices, visit book.getfoundry.sh.

Conclusion

UUPS proxies enable secure, future-proof DeFi and NFT contracts. Follow this guide, test rigorously, and audit before 2026 mainnet. Stay updated with Solidity improvements for even safer upgrades.

Share

Comments

to leave a comment.

No comments yet. Be the first!