Comparaison·8 min de lecture·Par Solingo

Transparent vs UUPS vs Beacon Proxy — Quel Pattern Utiliser

Comparaison détaillée des trois patterns de proxy pour smart contracts upgradeables : Transparent Proxy, UUPS et Beacon Proxy. Mécanismes, coûts gas, sécurité et recommandations.

# Transparent vs UUPS vs Beacon Proxy — Quel Pattern Utiliser

Les smart contracts sont immuables par design, mais cette immuabilité devient un problème quand vous devez corriger un bug ou ajouter des features. Les patterns de proxy offrent une solution élégante : séparer la logique (upgradeable) du storage (permanent).

En 2026, trois patterns dominent : Transparent Proxy, UUPS (Universal Upgradeable Proxy Standard) et Beacon Proxy. Chacun avec des trade-offs distincts.

Le Problème de l'Upgradeabilité

Pourquoi les Proxies ?

Sans proxy :

// V1 : Deploy initial

contract Token {

mapping(address => uint256) public balances;

function transfer(address to, uint256 amount) external {

balances[msg.sender] -= amount;

balances[to] += amount;

}

}

// Bug découvert : pas de check sur balance suffisante !

// → Impossible de corriger sans redeploy + migration des données

Avec proxy :

User → Proxy (storage) → Implementation (logic)

(delegatecall)

Le proxy utilise delegatecall pour exécuter le code de l'implementation DANS le contexte du proxy (donc avec le storage du proxy).

Transparent Proxy Pattern

Architecture

Le Transparent Proxy sépare strictement les appels entre admin (qui peut upgrade) et users (qui utilisent le contrat).

Principe :

  • Si msg.sender == admin → exécute les fonctions admin du proxy
  • Sinon → delegatecall vers l'implementation

Diagramme :

┌─────────────────────────────┐

│ TransparentProxy │

│ - admin: address │

│ - implementation: address │

│ - storage variables │

├─────────────────────────────┤

│ fallback() { │

│ if (msg.sender == admin)│

│ → admin functions │

│ else │

│ → delegatecall impl │

│ } │

└─────────────────────────────┘

↓ delegatecall

┌─────────────────────────────┐

│ Implementation V1 │

│ - business logic │

└─────────────────────────────┘

Code (OpenZeppelin) :

// Déploiement avec OpenZeppelin

import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";

contract MyImplementation {

uint256 public value;

function initialize(uint256 _value) public {

value = _value;

}

function setValue(uint256 _value) public {

value = _value;

}

}

// Script de deploy

ProxyAdmin admin = new ProxyAdmin();

MyImplementation impl = new MyImplementation();

TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(

address(impl),

address(admin),

abi.encodeCall(impl.initialize, (42))

);

// Usage

MyImplementation proxied = MyImplementation(address(proxy));

proxied.setValue(100); // Fonctionne

// Upgrade (depuis le ProxyAdmin)

MyImplementationV2 implV2 = new MyImplementationV2();

admin.upgrade(proxy, address(implV2));

Points Forts

1. Séparation Admin/User

Impossible pour un user d'appeler des fonctions admin, évite les collisions de sélecteurs.

2. Sécurité Prouvée

Pattern le plus ancien et le plus audité. Utilisé par des protocoles majeurs (anciennement Compound, MakerDAO).

3. ProxyAdmin Séparé

Le ProxyAdmin peut être un multisig ou un timelock, offrant une gouvernance robuste.

Points Faibles

1. Overhead Gas Important

Chaque appel user doit vérifier msg.sender == admin, ce qui coûte ~2,600 gas supplémentaires par call.

2. Complexité

Trois contrats à déployer : Implementation + Proxy + ProxyAdmin.

3. Collision de Sélecteurs Possible

Si l'implementation a une fonction avec le même selector qu'une fonction admin du proxy, le user ne pourra jamais l'appeler.

Coûts Gas

| Operation | Gas |

|-----------|-----|

| Deploy Proxy | ~500,000 |

| Deploy ProxyAdmin | ~250,000 |

| Call function (user) | +2,600 gas overhead |

| Upgrade | ~30,000 |

UUPS Pattern (Universal Upgradeable Proxy Standard)

Architecture

UUPS inverse la logique : la fonction upgradeTo est dans l'implementation, pas dans le proxy. Le proxy est minimal (juste un fallback).

Principe :

  • Proxy = dumb forwarder (juste delegatecall)
  • Implementation = contient la logique d'upgrade

Diagramme :

┌─────────────────────────────┐

│ ERC1967Proxy (minimal) │

│ - implementation: address │

│ - storage variables │

├─────────────────────────────┤

│ fallback() { │

│ delegatecall(impl) │

│ } │

└─────────────────────────────┘

↓ delegatecall

┌─────────────────────────────┐

│ UUPSImplementation │

│ - business logic │

│ - upgradeTo(address) │ ← Logic d'upgrade ICI

└─────────────────────────────┘

Code (OpenZeppelin) :

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

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

contract MyImplementationUUPS is UUPSUpgradeable, OwnableUpgradeable {

uint256 public value;

function initialize(uint256 _value) public initializer {

__Ownable_init();

__UUPSUpgradeable_init();

value = _value;

}

function setValue(uint256 _value) public {

value = _value;

}

// CRITIQUE : Protéger upgradeTo avec onlyOwner

function _authorizeUpgrade(address newImplementation)

internal

override

onlyOwner

{}

}

// Deploy

MyImplementationUUPS impl = new MyImplementationUUPS();

ERC1967Proxy proxy = new ERC1967Proxy(

address(impl),

abi.encodeCall(impl.initialize, (42))

);

// Usage

MyImplementationUUPS proxied = MyImplementationUUPS(address(proxy));

// Upgrade (depuis le owner)

MyImplementationUUPSV2 implV2 = new MyImplementationUUPSV2();

proxied.upgradeTo(address(implV2));

Points Forts

1. Gas Optimisé

Pas de check admin dans le proxy, économise ~2,600 gas par call.

2. Proxy Minimal

Un seul contrat proxy léger (ERC1967Proxy ~50 lignes).

3. Flexibilité

La logique d'upgrade peut être customisée par implementation (timelocks, voting, etc.).

Points Faibles

1. Risque d'Erreur Critique

Si vous déployez une implementation SANS la fonction upgradeTo correctement protégée, le contrat devient définitivement non-upgradeable.

2. Complexité pour les Devs

Chaque nouvelle implementation DOIT hériter de UUPSUpgradeable et implémenter _authorizeUpgrade.

3. Attack Surface dans l'Implementation

Un bug dans _authorizeUpgrade peut permettre à un attacker de prendre le contrôle du contrat.

Coûts Gas

| Operation | Gas |

|-----------|-----|

| Deploy Proxy | ~200,000 |

| Call function (user) | Baseline (pas d'overhead) |

| Upgrade | ~30,000 |

Économies : ~60% moins cher en déploiement que Transparent, ~10% moins cher par call.

Beacon Proxy Pattern

Architecture

Le Beacon Proxy est conçu pour upgrader multiples proxies en une seule transaction. Tous les proxies pointent vers un Beacon qui, lui, pointe vers l'implementation.

Principe :

  • Beacon = contrat qui stocke l'adresse de l'implementation
  • Multiples proxies = pointent tous vers le même Beacon
  • Upgrade = changer l'implementation dans le Beacon (tous les proxies sont upgradés simultanément)

Diagramme :

Proxy1 ──┐

Proxy2 ──┼──→ Beacon → Implementation

Proxy3 ──┘

Upgrade:

admin.upgradeTo(newImpl) → Beacon.implementation = newImpl

→ Tous les Proxies utilisent automatiquement newImpl

Code (OpenZeppelin) :

import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";

import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";

contract MyImplementation {

uint256 public value;

function initialize(uint256 _value) public {

value = _value;

}

}

// Deploy

MyImplementation impl = new MyImplementation();

UpgradeableBeacon beacon = new UpgradeableBeacon(address(impl));

// Déployer 100 proxies pointant vers le même Beacon

for (uint i = 0; i < 100; i++) {

BeaconProxy proxy = new BeaconProxy(

address(beacon),

abi.encodeCall(impl.initialize, (i))

);

}

// Upgrade TOUS les proxies en une transaction

MyImplementationV2 implV2 = new MyImplementationV2();

beacon.upgradeTo(address(implV2));

Points Forts

1. Upgrade Massif

Upgrader 1000 contrats en une seule transaction (au lieu de 1000).

2. Économies de Gas

Si vous avez N proxies, le coût d'upgrade est fixe (~30k gas) au lieu de N × 30k.

3. Coordination

Garantit que tous les proxies utilisent la même version de l'implementation.

Points Faibles

1. Storage EXTRA

Chaque call doit d'abord lire le Beacon (SLOAD supplémentaire = ~2,100 gas).

2. Single Point of Failure

Si le Beacon est compromis, TOUS les proxies sont compromis.

3. Use Case Limité

Utile uniquement si vous avez des dizaines/centaines de proxies (ex: game items, multi-tenant SaaS).

Coûts Gas

| Operation | Gas |

|-----------|-----|

| Deploy Beacon | ~300,000 |

| Deploy Proxy | ~150,000 |

| Call function (user) | +2,100 gas overhead |

| Upgrade (100 proxies) | ~30,000 (total, pas par proxy) |

Use case : Si vous avez 50+ proxies, Beacon devient plus économique que UUPS/Transparent.

Comparaison Directe

Tableau Synthétique

| Critère | Transparent | UUPS | Beacon |

|---------|------------|------|--------|

| Gas déploiement | Haut | Bas | Moyen |

| Gas par call | +2,600 | Baseline | +2,100 |

| Complexité | Haute | Moyenne | Moyenne |

| Sécurité | Prouvée | Attention bugs | Single point failure |

| Flexibilité upgrade | Moyenne | Haute | Haute (massif) |

| Use case | Single proxy | Single proxy | Multiple proxies |

Code Comparison : Upgrade Flow

Transparent :

// Depuis le ProxyAdmin (multisig)

proxyAdmin.upgrade(transparentProxy, newImplementation);

UUPS :

// Depuis le owner du contrat (peut être un DAO)

MyContract(proxy).upgradeTo(newImplementation);

Beacon :

// Depuis le owner du Beacon

beacon.upgradeTo(newImplementation);

// → Tous les proxies utilisent immédiatement newImplementation

Storage Collisions : Le Danger Commun

Quel que soit le pattern, une erreur de storage layout peut détruire vos données.

Exemple de Collision

V1 :

contract TokenV1 {

uint256 public totalSupply; // Slot 0

mapping(address => uint256) public balances; // Slot 1

}

V2 (FAUX - collision) :

contract TokenV2 {

address public owner; // Slot 0 ← COLLISION avec totalSupply !

uint256 public totalSupply; // Slot 1 ← COLLISION avec balances !

mapping(address => uint256) public balances; // Slot 2

}

Après upgrade :

  • totalSupply est interprété comme une adresse
  • balances sont interprétés comme un uint256
  • Toutes les données sont corrompues

Protection avec Storage Gaps

contract TokenV1 {

uint256 public totalSupply;

mapping(address => uint256) public balances;

// Réserve 50 slots pour futures variables

uint256[50] private __gap;

}

contract TokenV2 is TokenV1 {

// On peut ajouter des variables ICI sans collision

address public owner;

// Réduire le gap de 1 (on a utilisé 1 slot)

uint256[49] private __gap;

}

Best practice : Toujours inclure un __gap de 50 slots dans vos contrats upgradeables.

Initializers vs Constructors

Les proxies ne peuvent pas utiliser les constructors (exécutés au déploiement, pas dans le contexte du proxy).

FAUX :

contract Token {

address public owner;

constructor() {

owner = msg.sender; // Stocke dans l'implementation, PAS le proxy !

}

}

CORRECT :

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

contract Token is Initializable {

address public owner;

function initialize() public initializer {

owner = msg.sender; // Stocke dans le proxy ✓

}

}

Le modifier initializer garantit qu'on ne peut appeler initialize qu'une seule fois.

Matrice de Décision

Choisissez Transparent Proxy si :

✅ Vous voulez la sécurité maximale (pattern prouvé depuis 2018)

✅ Vous avez un projet high-value (DeFi avec TVL >$10M)

✅ Vous préférez la simplicité d'usage (moins de risque d'erreur)

✅ Le gas overhead est acceptable

❌ Vous optimisez pour le gas

❌ Vous avez besoin de custom upgrade logic

Choisissez UUPS si :

✅ Vous optimisez le gas (apps grand public, gaming)

✅ Vous voulez une flexibilité maximale (custom upgrade logic)

✅ Votre équipe maîtrise les subtilités des proxies

✅ Vous utilisez OpenZeppelin Defender (protection contre erreurs)

❌ Vous débutez avec les proxies

❌ Vous voulez zéro risque d'erreur d'implémentation

Choisissez Beacon Proxy si :

✅ Vous déployez 50+ contrats identiques (ex: game items, multi-tenant SaaS)

✅ Vous voulez upgrader tous les contrats en une transaction

✅ Vous acceptez le overhead de +2,100 gas par call

❌ Vous n'avez qu'un seul contrat

❌ Vous voulez des implementations différentes par proxy

Tendances 2026

Statistiques d'adoption (base : top 100 protocoles DeFi) :

  • Transparent Proxy : 45% (legacy projects)
  • UUPS : 40% (nouveaux projects)
  • Beacon Proxy : 10% (gaming, multi-contract apps)
  • No proxy (immutable) : 5% (protocoles décentralisés radicaux)

Évolution : UUPS gagne du terrain grâce à l'optimisation gas, mais Transparent reste le standard pour les protocoles critiques.

Recommandations Finales

Pour un projet DeFi standard : UUPS avec OpenZeppelin Defender pour la protection.

Pour un protocole critique (>$50M TVL) : Transparent Proxy pour la sécurité prouvée.

Pour une application multi-instance : Beacon Proxy (ex: game avec 1000 types d'items).

Pour un projet éducatif : Commencez avec Transparent (moins de risque d'erreur), migrez vers UUPS quand vous maîtrisez les concepts.

Conseil pro : Utilisez toujours les implémentations OpenZeppelin plutôt que de coder vos propres proxies. Ces contrats ont été audités des dizaines de fois et sont battle-tested.

Sur Solingo, vous pouvez expérimenter avec les trois patterns de proxy dans un environnement interactif et comprendre les nuances de storage layout avant de déployer en production.

Prêt à mettre en pratique ?

Applique ces concepts avec des exercices interactifs sur Solingo.

Commencer gratuitement