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