Comparaison·8 min de lecture·Par Solingo

Transparent vs UUPS vs Beacon Proxy — Which Pattern to Use

Comparaison detaillee des trois patterns de proxy pour smart contracts upgradeables : Transparent Proxy, UUPS et Beacon Proxy. Mecanismes, couts gas, securite et recommendations.

# Transparent vs UUPS vs Beacon Proxy — Which Pattern to Use

Les smart contracts sont immuables par design, mais cette immutabilite devient un probleme quand vous devez corriger un bug ou ajouter des features. Les patterns de proxy offrent une solution elegante : separer 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 Probleme de l'Upgradeabilite

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 decouvert : pas de check sur balance suffisante !

// → Impossible de corriger sans redeploy + migration des donnees

Avec proxy :

User → Proxy (storage) → Implementation (logic)

(delegatecall)

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

Transparent Proxy Pattern

Architecture

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

Principe :

  • Si msg.sender == admin → execute 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) :

// Deploiement 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. Separation Admin/User

Impossible pour un user d'appeler des fonctions admin, evite les collisions de selecteurs.

2. Securite Prouvee

Pattern le plus ancien et le plus audite. Utilise par des protocoles majeurs (anciennement Compound, MakerDAO).

3. ProxyAdmin Separe

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

Points Faibles

1. Overhead Gas Important

Chaque appel user doit verifier msg.sender == admin, ce qui coute ~2,600 gas supplementaires par call.

2. Complexite

Trois contrats a deployer : Implementation + Proxy + ProxyAdmin.

3. Collision de Selecteurs Possible

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

Couts 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 : Proteger 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 Optimise

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

2. Proxy Minimal

Un seul contrat proxy leger (ERC1967Proxy ~50 lignes).

3. Flexibilite

La logique d'upgrade peut etre customisee par implementation (timelocks, voting, etc.).

Points Faibles

1. Risque d'Erreur Critique

Si vous deployez une implementation SANS la fonction upgradeTo correctement protegee, le contrat devient definitivement non-upgradeable.

2. Complexite pour les Devs

Chaque nouvelle implementation DOIT heriter de UUPSUpgradeable et implementer _authorizeUpgrade.

3. Attack Surface dans l'Implementation

Un bug dans _authorizeUpgrade peut permettre a un attacker de prendre le controle du contrat.

Couts Gas

| Operation | Gas |

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

| Deploy Proxy | ~200,000 |

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

| Upgrade | ~30,000 |

Economies : ~60% moins cher en deployment que Transparent, ~10% moins cher par call.

Beacon Proxy Pattern

Architecture

Le Beacon Proxy est concu 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 meme Beacon
  • Upgrade = changer l'implementation dans le Beacon (tous les proxies sont upgrades simultanement)

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

// Deployer 100 proxies pointant vers le meme 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. Economies de Gas

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

3. Coordination

Garantit que tous les proxies utilisent la meme version de l'implementation.

Points Faibles

1. Storage EXTRA

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

2. Single Point of Failure

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

3. Use Case Limite

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

Couts 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 economique que UUPS/Transparent.

Comparaison Directe

Tableau Synthetique

| Critere | Transparent | UUPS | Beacon |

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

| Gas deployment | Haut | Bas | Moyen |

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

| Complexite | Haute | Moyenne | Moyenne |

| Securite | Prouvee | Attention bugs | Single point failure |

| Flexibilite 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 etre un DAO)

MyContract(proxy).upgradeTo(newImplementation);

Beacon :

// Depuis le owner du Beacon

beacon.upgradeTo(newImplementation);

// → Tous les proxies utilisent immediatement newImplementation

Storage Collisions : Le Danger Commun

Quel que soit le pattern, une erreur de storage layout peut detruire vos donnees.

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

}

Apres upgrade :

  • totalSupply est interprete comme une adresse
  • balances sont interpretes comme un uint256
  • Toutes les donnees sont corrompues

Protection avec Storage Gaps

contract TokenV1 {

uint256 public totalSupply;

mapping(address => uint256) public balances;

// Reserve 50 slots pour futures variables

uint256[50] private __gap;

}

contract TokenV2 is TokenV1 {

// On peut ajouter des variables ICI sans collision

address public owner;

// Reduire le gap de 1 (on a utilise 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 (executes au deployment, 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.

Decision Matrix

Choisissez Transparent Proxy si :

✅ Vous voulez la securite maximale (pattern prouve depuis 2018)

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

✅ Vous preferez la simplicite 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 flexibilite maximale (custom upgrade logic)

✅ Votre equipe maitrise les subtilites des proxies

✅ Vous utilisez OpenZeppelin Defender (protection contre erreurs)

❌ Vous debutez avec les proxies

❌ Vous voulez zero risque d'erreur d'implementation

Choisissez Beacon Proxy si :

✅ Vous deployez 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 differentes 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% (protocols decentralises radicaux)

Evolution : UUPS gagne du terrain grace a 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 securite prouvee.

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

Pour un projet educatif : Commencez avec Transparent (moins de risque d'erreur), migrez vers UUPS quand vous maitrisez les concepts.

Conseil pro : Utilisez toujours les implementations OpenZeppelin plutot que de coder vos propres proxies. Ces contrats ont ete audites des dizaines de fois et sont battle-tested.

Sur Solingo, vous pouvez experimenter avec les trois patterns de proxy dans un environnement interactif et comprendre les nuances de storage layout avant de deployer en production.

Prêt à mettre en pratique ?

Applique ces concepts avec des exercices interactifs sur Solingo.

Commencer gratuitement