# 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 :
totalSupplyest interprété comme une adresse
balancessont 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.