NamedBeacon i Proxy: optymalna implementacja aktualizowalnych proxy w Solidity
Programiści często napotykają nieefektywność standardowego BeaconProxy: oddzielny kontrakt do przechowywania jednego adresu implementacji, powtarzające się odniesienia w immutable i storage, nadmiarowa logika administracyjna w każdym proxy. Para kontraktów NamedBeacon i NamedBeaconProxy rozwiązuje te problemy, minimalizując zużycie gazu i rozmiar kodu bajtowego.
NamedBeacon centralizuje zarządzanie adresami implementacji za pomocą identyfikatorów typu bytes32. Proxy przechowuje tylko stałe odniesienia do beacona i referenceId, dynamicznie pobierając aktualną implementację.
NamedBeacon: centralne magazynowanie implementacji
Ten kontrakt implementuje mapping(bytes32 => address) do przechowywania adresów. Dwie kluczowe funkcje:
registerImplementation(bytes32 _referenceId, address _implementation)— rejestracja, dostępna tylko dla ownera.getImplementation(bytes32 _referenceId)— odczyt adresu po ID, zwraca address(0), jeśli nie znaleziono.
Zarządzanie lokalizowane: owner sprawdzany w registerImplementation. Weryfikacja implementacji wyklucza puste adresy bez kodu. Dla kompatybilności z OpenZeppelin dodana funkcja implementation() zwracająca address(this).
Przechowywanie w storage pasuje do scenariuszy niemodyfikowalnych po wdrożeniu. Brak SlotStorage upraszcza kod, ale ogranicza możliwości aktualizacji beacona.
Pełny kod 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 This must be overwritten by your custom auth logic
address public owner;
/// @notice Main storage of implementations
mapping(bytes32 implId => address implAddress) internal implementations;
error InvalidImplementation(address implementation);
error UnauthorizedAccess();
// ctor
constructor (address _owner) {
/// @dev This must be overwritten by your custom auth logic
require (_owner != address(0));
owner = _owner;
}
/// @notice Returns an address of an implementation, registered under _referenceId. If no reference is found, returns address(0).
/// @param _referenceId Unique ref id of the referenced contract
function getImplementation(bytes32 _referenceId) external view returns (address _implementation) {
return implementations[_referenceId];
}
/// @notice Registers implementation _implementation under given _referenceId.
/// @param _referenceId Unique ref id of the referenced contract
/// @param _implementation Address of implementation
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 Special function to mimic beacon functionality from OZ
/// @dev Exists only to be compatible with OZ BeaconProxy
/// @dev BeaconProxy.ctor() => ERC1967Utils.upgradeBeaconToAndCall(beacon, data) => _setBeacon(address newBeacon) => address beaconImplementation = IBeacon(newBeacon).implementation();
/// @dev 1. this should return a valid address
/// @dev 2. AND it should be a contract: if (beaconImplementation.code.length == 0) { revert }
/// @dev Similar issue is in ERC1967Utils.upgradeBeaconToAndCall(address newBeacon, bytes memory data),
/// @dev on line `Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);`
/// @dev Given this,
function implementation() external view returns (address _current) {
return address(this);
}
}
NamedBeaconProxy: minimalny proxy
Dziedziczy po Proxy i używa Address z OpenZeppelin. Pola immutable: beacon i implementationReferenceId — bez slotów storage.
W konstruktorze:
- Walidacja beacona (code.length > 0).
- Pobranie implementacji z beacona po referenceId, sprawdzenie code.length.
- Opcjonalny delegatecall do inicjalizacji z _data.
Przesłonięta funkcja _implementation() zwraca INamedBeacon(beacon).getImplementation(implementationReferenceId).
Obsługa receive() dla payable, błąd NonPayable przy msg.value > 0 bez _data.
Pełny kod 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 {
// set beacon
if (_beacon.code.length == 0) {
revert InvalidBeacon(_beacon);
}
beacon = _beacon;
// set implementation ref id
address implementationFromBeacon = INamedBeacon(_beacon).getImplementation(_implementationReferenceId);
if (implementationFromBeacon.code.length == 0) {
revert InvalidImplementation(implementationFromBeacon);
}
implementationReferenceId = _implementationReferenceId;
// initialize if data is provided
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();
}
}
}
Proces wdrożenia i użytkowania
- Wdrożenie NamedBeacon z ownerem.
- Wdrożenie implementacji.
- Rejestracja:
registerImplementation(keccak256('v1'), implementationAddress). - Wdrożenie NamedBeaconProxy(beacon, referenceId, initData).
Sprawdzenia w proxy można wyłączyć dla oszczędności gazu podczas wywołań. Logika ownera jest dostosowywana w dziedziczonej klasie NamedBeacon.
Co jest ważne
- Centralne przechowywanie adresów implementacji za pomocą identyfikatorów bytes32 zmniejsza duplikację kodu.
- Stałe odniesienia w proxy eliminują użycie slotów storage.
- Minimalny rozmiar proxy: brak logiki administracyjnej, tylko delegatecall do beacona.
- Kompatybilność z OZ: funkcja implementation() dla upgradeBeaconToAndCall.
- Solidity 0.8.33, Hardhat 3.1.6 — gotowe do produkcji z testami.
— Editorial Team
Brak komentarzy.