Tutoriel·9 min de lecture·Par Solingo

Les Patterns Proxy Expliqués — Transparent, UUPS et Beacon

Smart contracts immuables mais upgradables ? Comparaison détaillée des trois architectures proxy majeures avec exemples de code.

# Les Patterns Proxy Expliqués — Transparent, UUPS et Beacon

Les smart contracts sont immuables. Mais les bugs, eux, ne le sont pas. D'où les proxies.

Le Problème : Immuabilité

// V1 : Déployé sur mainnet

contract Token {

mapping(address => uint256) public balances;

function transfer(address to, uint256 amount) external {

balances[msg.sender] -= amount;

balances[to] += amount; // ❌ Bug : pas de check overflow

}

}

// Impossible de fix sans redéployer

// → Tous les utilisateurs doivent migrer

// → Perte de liquidity/composability

La Solution : Pattern Proxy

Un proxy = contrat qui délègue les appels à une implementation.

// Proxy (immuable)

contract Proxy {

address public implementation;

fallback() external payable {

address impl = implementation;

assembly {

calldatacopy(0, 0, calldatasize())

let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)

returndatacopy(0, 0, returndatasize())

switch result

case 0 { revert(0, returndatasize()) }

default { return(0, returndatasize()) }

}

}

}

// Implementation V1

contract TokenV1 {

mapping(address => uint256) public balances;

function transfer(address to, uint256 amount) external {

balances[msg.sender] -= amount;

balances[to] += amount;

}

}

// Implementation V2 (fix du bug)

contract TokenV2 {

mapping(address => uint256) public balances;

function transfer(address to, uint256 amount) external {

require(balances[msg.sender] >= amount, "Insufficient balance");

balances[msg.sender] -= amount;

balances[to] += amount;

}

}

Upgrade :

proxy.setImplementation(address(tokenV2));

// Même adresse proxy, nouvelle logique

Pattern 1 : Transparent Proxy

Principe : le proxy gère l'upgrade logic.

contract TransparentProxy {

address private immutable ADMIN;

address private implementation;

constructor(address _admin, address _implementation) {

ADMIN = _admin;

implementation = _implementation;

}

modifier ifAdmin() {

if (msg.sender == ADMIN) {

_;

} else {

_fallback();

}

}

function upgradeTo(address newImplementation) external ifAdmin {

implementation = newImplementation;

}

function _fallback() private {

// delegatecall vers implementation

}

fallback() external payable {

_fallback();

}

}

Avantages

Simple : admin upgrade, users utilisent

Sécurisé : séparation admin/user claire

Inconvénients

Gas : check admin à chaque call (SLOAD = 2100 gas)

Fonction collision : si implementation a une fonction upgradeTo(), conflit

Quand l'Utiliser

  • Protocoles avec gouvernance centralisée
  • NFT projects (admin = multisig)

Exemple : OpenZeppelin TransparentUpgradeableProxy

Pattern 2 : UUPS (Universal Upgradeable Proxy Standard)

Principe : l'implementation gère son propre upgrade logic.

// Proxy (minimal)

contract UUPSProxy {

address private implementation;

constructor(address _implementation) {

implementation = _implementation;

}

fallback() external payable {

address impl = implementation;

assembly {

calldatacopy(0, 0, calldatasize())

let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)

returndatacopy(0, 0, returndatasize())

switch result

case 0 { revert(0, returndatasize()) }

default { return(0, returndatasize()) }

}

}

}

// Implementation (contient upgrade logic)

contract TokenUUPS is Initializable, UUPSUpgradeable, OwnableUpgradeable {

mapping(address => uint256) public balances;

function initialize() public initializer {

__Ownable_init();

__UUPSUpgradeable_init();

}

function transfer(address to, uint256 amount) external {

balances[msg.sender] -= amount;

balances[to] += amount;

}

// ✅ Upgrade logic dans l'implementation

function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}

}

Avantages

Gas : pas de check admin dans le proxy (économie ~2K gas/call)

Flexibilité : upgrade logic customisable

Pas de collision : upgrade functions dans l'implementation

Inconvénients

Complexité : upgrade logic doit être correcte dans chaque version

Risque : si une version oublie la fonction upgrade, le contrat devient immuable

Quand l'Utiliser

  • Protocoles à haut volume (DEX, lending)
  • DeFi avec gouvernance on-chain

Exemple : Compound V3

Pattern 3 : Beacon Proxy

Principe : plusieurs proxies pointent vers un même Beacon qui stocke l'implementation.

// Beacon

contract UpgradeableBeacon is Ownable {

address public implementation;

event Upgraded(address indexed implementation);

constructor(address _implementation) {

implementation = _implementation;

}

function upgradeTo(address newImplementation) external onlyOwner {

implementation = newImplementation;

emit Upgraded(newImplementation);

}

}

// Beacon Proxy

contract BeaconProxy {

address private immutable BEACON;

constructor(address beacon) {

BEACON = beacon;

}

function _implementation() internal view returns (address) {

return IBeacon(BEACON).implementation();

}

fallback() external payable {

address impl = _implementation();

assembly {

calldatacopy(0, 0, calldatasize())

let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)

returndatacopy(0, 0, returndatasize())

switch result

case 0 { revert(0, returndatasize()) }

default { return(0, returndatasize()) }

}

}

}

Usage

// Deploy

UpgradeableBeacon beacon = new UpgradeableBeacon(tokenV1);

// Deploy 1000 proxies

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

BeaconProxy proxy = new BeaconProxy(address(beacon));

// Chaque proxy pointe vers le beacon

}

// Upgrade ALL proxies en une transaction

beacon.upgradeTo(address(tokenV2));

// → Les 1000 proxies utilisent maintenant tokenV2

Avantages

Batch upgrade : un seul upgrade pour N proxies

Gas deployment : proxy minimal = moins cher

Coordination : garantit que tous les proxies sont sur la même version

Inconvénients

Single point of failure : si beacon compromis, tous les proxies impactés

Rigidité : tous les proxies doivent upgrader en même temps

Quand l'Utiliser

  • Factory patterns (ex : créer 1000 vaults identiques)
  • Multi-chain deployments (même logic sur 10 chains)

Exemple : Dharma, OpenZeppelin Clones

Comparaison

| Feature | Transparent | UUPS | Beacon |

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

| Gas (deploy) | 🟡 Moyen | 🟢 Bas | 🟢 Très bas |

| Gas (call) | 🔴 Élevé | 🟢 Bas | 🟡 Moyen |

| Upgrade logic | Dans proxy | Dans impl | Dans beacon |

| Batch upgrade | ❌ | ❌ | ✅ |

| Risk si bug | 🟢 Faible | 🔴 Élevé | 🟡 Moyen |

| Adoption | 🔴 Déclin | 🟢 Standard | 🟡 Niche |

Storage Collisions

Danger : le proxy et l'implementation partagent le même storage.

// ❌ MAUVAIS

contract Proxy {

address public implementation; // slot 0

}

contract Implementation {

address public owner; // slot 0 aussi !

// → Collision : owner écrase implementation

}

// ✅ BON : EIP-1967 Storage Slots

contract Proxy {

// slot = keccak256("eip1967.proxy.implementation") - 1

bytes32 private constant IMPLEMENTATION_SLOT =

0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

function _implementation() internal view returns (address impl) {

assembly {

impl := sload(IMPLEMENTATION_SLOT)

}

}

}

Initializers vs Constructors

Les proxies ne peuvent pas utiliser de constructors (car le code n'est pas dans le proxy).

// ❌ Ne fonctionne pas avec un proxy

contract Token {

address public owner;

constructor() {

owner = msg.sender; // S'exécute dans l'implementation, pas le proxy

}

}

// ✅ Utilisez un initializer

contract Token is Initializable {

address public owner;

function initialize() public initializer {

owner = msg.sender; // S'exécute via delegatecall depuis le proxy

}

}

Sécurité : Checklist

  • [ ] Initializers protected : initializer modifier ou check manuel
  • [ ] Selfdestruct forbidden : une implementation ne doit JAMAIS avoir selfdestruct
  • [ ] Delegatecall safe : attention aux delegatecalls dans l'implementation
  • [ ] Storage layout preserved : ne jamais réordonner les variables en V2
  • [ ] Upgrade governance : multisig ou timelock sur upgrade functions

Tools

# Vérifier les storage collisions

forge inspect MyContract storage-layout

# Tester un upgrade

forge test --match-test testUpgrade

# Valider la compatibilité V1 → V2

npx @openzeppelin/upgrades-core validate

Ressources

Les proxies sont puissants mais dangereux. Auditez toujours votre upgrade logic.

Prêt à mettre en pratique ?

Applique ces concepts avec des exercices interactifs sur Solingo.

Commencer gratuitement