NamedBeacon und Proxy: Optimierte Implementierung für upgradefähige Solidity-Verträge
Entwickler stoßen oft auf ineffiziente Strukturen beim Standard-BeaconProxy: Ein separates Contract nur zur Speicherung einer Implementationsadresse, doppelte Referenzen in immutable und Storage sowie redundante Admin-Logik in jedem Proxy. Die vorgeschlagene Kombination aus NamedBeacon und NamedBeaconProxy beseitigt diese Probleme und minimiert Gas-Kosten sowie Bytecode-Größe.
NamedBeacon zentralisiert die Verwaltung von Implementationsadressen mithilfe von stringähnlichen bytes32-Identifikatoren. Der Proxy speichert lediglich immutable Referenzen auf den Beacon und die ReferenceId und holt die aktuelle Implementierung dynamisch zur Laufzeit ab.
NamedBeacon: Zentrale Implementations-Registry
Dieser Contract implementiert ein mapping(bytes32 => address), um Adressen zu speichern. Zwei Schlüsselfunktionen:
registerImplementation(bytes32 _referenceId, address _implementation)— Registrierung, nur vom Owner möglich.getImplementation(bytes32 _referenceId)— Ruft die Adresse anhand der ID ab, gibt address(0) zurück, falls nicht gefunden.
Die Administration ist zentralisiert: Owner-Prüfungen werden in registerImplementation durchgeführt. Implementationsvalidierung verhindert Null-Adressen ohne Code. Für Kompatibilität mit OpenZeppelin ist eine implementation()-Funktion enthalten, die address(this) zurückgibt.
Der Speicherverbrauch ist ideal für immutable Szenarien nach dem Deployment. Das Fehlen von SlotStorage vereinfacht den Code, begrenzt aber die Beacon-Upgrades.
Vollständiger NamedBeacon-Code:
// 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 Dies muss durch Ihre benutzerdefinierte Auth-Logik überschrieben werden
address public owner;
/// @notice Hauptspeicher für Implementierungen
mapping(bytes32 => address) internal implementations;
error InvalidImplementation(address implementation);
error UnauthorizedAccess();
// ctor
constructor (address _owner) {
/// @dev Dies muss durch Ihre benutzerdefinierte Auth-Logik überschrieben werden
require (_owner != address(0));
owner = _owner;
}
/// @notice Gibt die Adresse einer Implementierung zurück, die unter _referenceId registriert ist. Falls keine Referenz gefunden wird, wird address(0) zurückgegeben.
/// @param _referenceId Eindeutige Referenz-ID des referenzierten Contracts
function getImplementation(bytes32 _referenceId) external view returns (address _implementation) {
return implementations[_referenceId];
}
/// @notice Registriert die Implementierung _implementation unter der angegebenen _referenceId.
/// @param _referenceId Eindeutige Referenz-ID des referenzierten Contracts
/// @param _implementation Adresse der Implementierung
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 Spezialfunktion zur Nachbildung der Beacon-Funktionalität aus OZ
/// @dev Existiert nur zur Kompatibilität mit OZ BeaconProxy
/// @dev BeaconProxy.ctor() => ERC1967Utils.upgradeBeaconToAndCall(beacon, data) => _setBeacon(address newBeacon) => address beaconImplementation = IBeacon(newBeacon).implementation();
/// @dev 1. Diese Funktion muss eine gültige Adresse zurückgeben
/// @dev 2. UND sie muss ein Contract sein: if (beaconImplementation.code.length == 0) { revert }
/// @dev Ähnliches Problem tritt in ERC1967Utils.upgradeBeaconToAndCall(address newBeacon, bytes memory data),
/// @dev Zeile `Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);`
/// @dev Angesichts dessen
function implementation() external view returns (address _current) {
return address(this);
}
}
NamedBeaconProxy: Minimalistischer upgradefähiger Proxy
Erweitert Proxy und nutzt Address aus OpenZeppelin. Immutable-Felder: beacon und implementationReferenceId — keine Speicherplätze werden genutzt.
Im Konstruktor:
- Validierung des Beacons (code.length > 0).
- Abruf der Implementierung über Beacon via referenceId, Prüfung von code.length.
- Optionaler delegatecall zum Initialisieren mit _data.
Überschriebene _implementation() gibt INamedBeacon(beacon).getImplementation(implementationReferenceId) zurück.
Unterstützt receive() für Zahlbarkeit, wirft NonPayable-Fehler, wenn msg.value > 0 ohne _data.
Vollständiger NamedBeaconProxy-Code:
// 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 {
// beacon setzen
if (_beacon.code.length == 0) {
revert InvalidBeacon(_beacon);
}
beacon = _beacon;
// Implementations-Referenz-ID setzen
address implementationFromBeacon = INamedBeacon(_beacon).getImplementation(_implementationReferenceId);
if (implementationFromBeacon.code.length == 0) {
revert InvalidImplementation(implementationFromBeacon);
}
implementationReferenceId = _implementationReferenceId;
// Initialisierung, falls Daten bereitgestellt werden
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();
}
}
}
Bereitstellung und Nutzung – Workflow
- NamedBeacon mit einem Owner bereitstellen.
- Implementierungsverträge bereitstellen.
- Registrieren:
registerImplementation(keccak256('v1'), implementationAddress). - NamedBeaconProxy(beacon, referenceId, initData) bereitstellen.
Validierungsprüfungen im Proxy können deaktiviert werden, um Gas bei Aufrufen zu sparen. Die Owner-Logik kann in einem abgeleiteten NamedBeacon-Contract angepasst werden.
Hauptvorteile
- Zentrale Speicherung von Implementationsadressen über bytes32-IDs reduziert Code-Duplikation.
- Immutable Referenzen im Proxy eliminieren Speicherplatznutzung.
- Minimaler Proxy-Größe: Keine Admin-Logik, nur delegatecall an den Beacon.
- Vollständige OpenZeppelin-Kompatibilität: Unterstützt
implementation()für upgradeBeaconToAndCall. - Für Produktion geeignet: Solidity 0.8.33, Hardhat 3.1.6, mit umfassender Testabdeckung.
— Editorial Team
Noch keine Kommentare.