NamedBeacon et Proxy : Implémentation optimisée pour les contrats Solidity upgradables
Les développeurs rencontrent souvent des inefficacités avec le BeaconProxy standard : un contrat distinct uniquement pour stocker une adresse d'implémentation, des références dupliquées dans les variables immuables et en mémoire, ainsi qu'une logique d'administration redondante dans chaque proxy. La paire proposée, NamedBeacon et NamedBeaconProxy, élimine ces problèmes, réduisant au minimum les coûts en gaz et la taille du bytecode.
NamedBeacon centralise la gestion des adresses d'implémentation à l'aide d'identifiants de type bytes32. Le proxy ne conserve que des références immuables vers le beacon et le referenceId, récupérant dynamiquement l'implémentation actuelle à l'exécution.
NamedBeacon : Registre centralisé des implémentations
Ce contrat implémente un mapping(bytes32 => address) pour stocker les adresses. Deux fonctions clés :
registerImplementation(bytes32 _referenceId, address _implementation)— inscription, disponible uniquement pour le propriétaire.getImplementation(bytes32 _referenceId)— récupère l'adresse par ID, renvoie address(0) si non trouvée.
L'administration est centralisée : les vérifications de propriétaire sont obligatoires dans registerImplementation. La validation des implémentations empêche les adresses nulles sans code. Pour assurer la compatibilité avec OpenZeppelin, une fonction implementation() est incluse, qui retourne address(this).
L'utilisation de stockage est optimale dans les scénarios immuables après déploiement. L'absence de SlotStorage simplifie le code mais limite les mises à jour du beacon.
Code complet de NamedBeacon :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.33;
interface INamedBeacon {
function getImplementation(bytes32 _referenceId) external view returns (address _implementation);
function registerImplementation(bytes32 _referenceId, address _implementation) external;
}
contract NamedBeacon {
/// @dev Cela doit être remplacé par votre logique d'authentification personnalisée
address public owner;
/// @notice Stockage principal des implémentations
mapping(bytes32 implId => address implAddress) internal implementations;
error InvalidImplementation(address implementation);
error UnauthorizedAccess();
// constructeur
constructor (address _owner) {
/// @dev Cela doit être remplacé par votre logique d'authentification personnalisée
require (_owner != address(0));
owner = _owner;
}
/// @notice Retourne l'adresse d'une implémentation enregistrée sous _referenceId. Si aucune référence n'est trouvée, retourne address(0).
/// @param _referenceId ID unique du contrat référencé
function getImplementation(bytes32 _referenceId) external view returns (address _implementation) {
return implementations[_referenceId];
}
/// @notice Enregistre l'implémentation _implementation sous le _referenceId donné.
/// @param _referenceId ID unique du contrat référencé
/// @param _implementation Adresse de l'implémentation
function registerImplementation(bytes32 _referenceId, address _implementation) external {
require (msg.sender == owner, UnauthorizedAccess());
if (_implementation != address(0) && _implementation.code.length == 0) {
revert InvalidImplementation(_implementation);
}
implementations[_referenceId] = _implementation;
}
/// @notice Fonction spéciale pour simuler la fonctionnalité de beacon d’OZ
/// @dev Existe uniquement pour la compatibilité avec OZ BeaconProxy
/// @dev BeaconProxy.ctor() => ERC1967Utils.upgradeBeaconToAndCall(beacon, data) => _setBeacon(address newBeacon) => address beaconImplementation = IBeacon(newBeacon).implementation();
/// @dev 1. cela doit retourner une adresse valide
/// @dev 2. ET elle doit être un contrat : if (beaconImplementation.code.length == 0) { revert }
/// @dev Problème similaire dans ERC1967Utils.upgradeBeaconToAndCall(address newBeacon, bytes memory data),
/// @dev ligne `Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);`
/// @dev Étant donné cela,
function implementation() external view returns (address _current) {
return address(this);
}
}
NamedBeaconProxy : Proxy minimaliste upgradable
Hérite de Proxy et utilise Address d’OpenZeppelin. Champs immuables : beacon et implementationReferenceId — aucun emplacement de stockage utilisé.
Dans le constructeur :
- Vérifie le beacon (code.length > 0).
- Récupère l'implémentation depuis le beacon via le referenceId, vérifie code.length.
- Appel optionnel delegatecall pour initialiser avec _data.
La fonction surchargée _implementation() retourne INamedBeacon(beacon).getImplementation(implementationReferenceId).
Prend en charge receive() pour les appels payables, lance une erreur NonPayable si msg.value > 0 sans _data.
Code complet de NamedBeaconProxy :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.33;
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { Proxy } from "@openzeppelin/contracts/proxy/Proxy.sol";
import { INamedBeacon } from "./NamedBeacon.sol";
contract NamedBeaconProxy is Proxy {
address private immutable beacon;
bytes32 private immutable implementationReferenceId;
error NonPayable();
error InvalidBeacon(address beacon);
error InvalidImplementation(address implementation);
error ImplementationNotFound(bytes32 implementationReferenceId);
constructor(address _beacon, bytes32 _implementationReferenceId, bytes memory _data) payable {
// définir le beacon
if (_beacon.code.length == 0) {
revert InvalidBeacon(_beacon);
}
beacon = _beacon;
// définir l'ID de référence de l'implémentation
address implementationFromBeacon = INamedBeacon(_beacon).getImplementation(_implementationReferenceId);
if (implementationFromBeacon.code.length == 0) {
revert InvalidImplementation(implementationFromBeacon);
}
implementationReferenceId = _implementationReferenceId;
// initialiser si des données sont fournies
if (_data.length > 0) {
Address.functionDelegateCall(implementationFromBeacon, _data);
} else {
_checkNonPayable();
}
}
function _implementation() internal view virtual override returns (address) {
return INamedBeacon(beacon).getImplementation(implementationReferenceId);
}
receive() external payable {}
function _checkNonPayable() private {
if (msg.value > 0) {
revert NonPayable();
}
}
}
Flux de déploiement et d'utilisation
- Déployer NamedBeacon avec un propriétaire.
- Déployer les contrats d'implémentation.
- Enregistrer :
registerImplementation(keccak256('v1'), implementationAddress). - Déployer NamedBeaconProxy(beacon, referenceId, initData).
Les vérifications de validation dans le proxy peuvent être désactivées pour économiser du gaz lors des appels. La logique de propriétaire peut être personnalisée dans un contrat dérivé de NamedBeacon.
Principaux avantages
- Stockage centralisé des adresses d'implémentation via des IDs bytes32 réduit la duplication de code.
- Références immuables dans le proxy éliminent l'utilisation de slots de stockage.
- Taille minimale du proxy : pas de logique d'admin, juste un delegatecall vers le beacon.
- Compatibilité complète avec OpenZeppelin : prend en charge
implementation()pour upgradeBeaconToAndCall. - Conçu pour la production : Solidity 0.8.33, Hardhat 3.1.6, avec couverture de tests complète.
— Editorial Team
Aucun commentaire pour le moment.