A propos Skills
Interactive Sandbox Web3 Engineering Security Lab Engineering Infrastructure Portfolio Audit Publications Changelog Veille Techno CTF Writeups Uses / Setup Blog Stats Now
Projets Contact

Upgradeable Proxy Deep Dive

Analyse complète du pattern proxy upgradeable en Solidity : UUPS vs Transparent, risques de collision de storage, patterns d'initialisation et gouvernance des upgrades.

Table des matières

    Introduction

    Smart contracts on Ethereum are immutable by design. Once deployed, their bytecode cannot be changed. This creates a fundamental tension: how do you fix bugs, patch vulnerabilities, or add features to a live contract managing millions of dollars? The answer is the proxy pattern — a delegate call architecture that separates storage from logic, allowing the logic contract to be swapped while preserving state.

    This article explores the two dominant proxy patterns (Transparent and UUPS), examines the storage collision risks that have caused real exploits, and walks through secure implementation practices.

    How Proxy Delegation Works

    At its core, a proxy contract uses delegatecall to execute code from an implementation contract in the context of the proxy's storage. The proxy holds all state variables and ETH, while the implementation holds only logic.

    1. Receives the call and hits the fallback() function
    2. Reads the implementation address from a specific storage slot
    3. Forwards the entire calldata via delegatecall
    4. Returns or reverts based on the implementation's response
    Proxy.sol
    1// SPDX-License-Identifier: MIT
    2pragma solidity ^0.8.20;
    3
    4contract Proxy {
    5 // EIP-1967 implementation slot
    6 bytes32 constant IMPL_SLOT =
    7 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    8
    9 fallback() external payable {
    10 address impl = _getImplementation();
    11 assembly {
    12 calldatacopy(0, 0, calldatasize())
    13 let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
    14 returndatacopy(0, 0, returndatasize())
    15 switch result
    16 case 0 { revert(0, returndatasize()) }
    17 default { return(0, returndatasize()) }
    18 }
    19 }
    20}

    Transparent Proxy Pattern (TPP)

    The Transparent Proxy (EIP-1967) solves the function selector clash problem by routing calls differently based on whether the caller is the admin or a regular user. Admin calls go to the proxy's own functions (upgrade, admin transfer), while all other calls are delegated to the implementation.

    The downside: every call requires an additional SLOAD to check the admin address, costing ~2,100 extra gas per transaction.

    UUPS Pattern

    Universal Upgradeable Proxy Standard (EIP-1822) moves the upgrade logic into the implementation contract itself. The proxy becomes extremely lightweight — just a fallback + constructor. This saves gas on every call but requires every new implementation to include the upgrade mechanism.

    If you deploy a new implementation without _authorizeUpgrade(), the proxy becomes permanently frozen. This is the most common UUPS footgun.

    Storage Collision Risks

    The most dangerous pitfall in proxy patterns is storage collision. Since the proxy and implementation share the same storage space (via delegatecall), any mismatch in storage layout between upgrades can corrupt data silently.

    • Never reorder or remove existing state variables
    • Only append new variables at the end
    • Use storage gaps (uint256[50] __gap) for future-proofing
    • Use OpenZeppelin's storage checker in CI

    Comparison: TPP vs UUPS

    Both patterns are battle-tested, but they serve different needs. TPP is simpler to reason about and harder to brick, while UUPS is more gas-efficient and flexible.

    Secure Implementation Checklist

    • Use initializer modifier instead of constructors
    • Call _disableInitializers() in implementation constructor
    • Implement _authorizeUpgrade with proper access control
    • Add storage gaps in all base contracts
    • Run OpenZeppelin's upgrade safety checks in CI
    • Use a timelock or multisig for upgrade governance
    • Test the full upgrade cycle in staging before mainnet
    Partager cet article

    Commentaires

    Les commentaires utilisent GitHub Discussions via Giscus. Connectez-vous avec GitHub pour participer.

    Activez Giscus sur votre repo GitHub pour afficher les commentaires.