NamedBeacon a Proxy: optimalizovaná implementace pro aktualizovatelné proxy v Solidity
Vývojáři často čelí neefektivitě standardního BeaconProxy: samostatný kontrakt pro uložení jediné adresy implementace, duplikace odkazů v immutable a storage, nadbytečný správní kód v každém proxy. Pár kontraktů NamedBeacon a NamedBeaconProxy odstraňuje tyto problémy, minimalizuje náklady na gas a velikost bytecode.
NamedBeacon centralizuje správu adres implementací pod řetězcovými identifikátory bytes32. Proxy uchovává pouze immutable odkazy na beacon a referenceId a dynamicky získává aktuální implementaci.
NamedBeacon: centrální úložiště implementací
Tento kontrakt implementuje mapping(bytes32 => address) pro uchování adres. Dvě klíčové funkce:
registerImplementation(bytes32 _referenceId, address _implementation)— registrace, dostupná pouze ownerovi.getImplementation(bytes32 _referenceId)— čtení adresy podle ID, vrátí address(0), pokud nenalezeno.
Správa je lokalizována: owner je ověřen v registerImplementation. Validace implementace vylučuje nulové adresy bez kódu. Pro kompatibilitu s OpenZeppelin byla přidána implementation(), která vrací address(this).
Uložení v storage je vhodné pro scénáře, které jsou po nasazení neměnné. Odstranění SlotStorage zjednodušuje kód, ale omezuje možnosti aktualizace beaconu.
Celý kód 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 Toto musí být přepsáno vaší vlastní logikou ověření
address public owner;
/// @notice Hlavní úložiště implementací
mapping(bytes32 implId => address implAddress) internal implementations;
error InvalidImplementation(address implementation);
error UnauthorizedAccess();
// ctor
constructor (address _owner) {
/// @dev Toto musí být přepsáno vaší vlastní logikou ověření
require (_owner != address(0));
owner = _owner;
}
/// @notice Vrátí adresu implementace registrovanou pod _referenceId. Pokud není nalezena žádná referenční hodnota, vrátí address(0).
/// @param _referenceId Jedinečný ID odkazu na referovaný kontrakt
function getImplementation(bytes32 _referenceId) external view returns (address _implementation) {
return implementations[_referenceId];
}
/// @notice Zaregistruje implementaci _implementation pod daným _referenceId.
/// @param _referenceId Jedinečný ID odkazu na referovaný kontrakt
/// @param _implementation Adresa implementace
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 Speciální funkce pro napodobení chování beaconu z OZ
/// @dev Existuje pouze kvůli kompatibilitě s OZ BeaconProxy
/// @dev BeaconProxy.ctor() => ERC1967Utils.upgradeBeaconToAndCall(beacon, data) => _setBeacon(address newBeacon) => address beaconImplementation = IBeacon(newBeacon).implementation();
/// @dev 1. toto by mělo vracet platnou adresu
/// @dev 2. A mělo by jít o kontrakt: if (beaconImplementation.code.length == 0) { revert }
/// @dev Podobný problém je i v ERC1967Utils.upgradeBeaconToAndCall(address newBeacon, bytes memory data),
/// @dev na řádku `Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);`
function implementation() external view returns (address _current) {
return address(this);
}
}
NamedBeaconProxy: minimalistické proxy
Dědí od Proxy a používá Address z OpenZeppelin. Immutable pole: beacon a implementationReferenceId — bez slotů storage.
V konstruktoru:
- Ověření beaconu (code.length > 0).
- Čtení implementace z beaconu podle referenceId, ověření code.length.
- Volitelný delegatecall pro inicializaci s _data.
Přepsaná _implementation() vrací INamedBeacon(beacon).getImplementation(implementationReferenceId).
Podpora receive() pro payable, chyba NonPayable při msg.value > 0 bez _data.
Celý kód 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 {
// nastavení beaconu
if (_beacon.code.length == 0) {
revert InvalidBeacon(_beacon);
}
beacon = _beacon;
// nastavení ID implementace
address implementationFromBeacon = INamedBeacon(_beacon).getImplementation(_implementationReferenceId);
if (implementationFromBeacon.code.length == 0) {
revert InvalidImplementation(implementationFromBeacon);
}
implementationReferenceId = _implementationReferenceId;
// inicializace, pokud je poskytnut data
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();
}
}
}
Postup nasazení a použití
- Nasazení NamedBeacon s ownerem.
- Nasazení implementací.
- Registrace:
registerImplementation(keccak256('v1'), implementationAddress). - Nasazení NamedBeaconProxy(beacon, referenceId, initData).
Ověření v proxy lze deaktivovat pro úsporu gasu při voláních. Logika ownera je přizpůsobitelná v dědičném kontraktu NamedBeacon.
Co je důležité
- Centralizované uchování adres implementací pod ID bytes32 snižuje duplikaci kódu.
- Immutable odkazy v proxy vylučují použití slotů storage.
- Minimální velikost proxy: žádná správní logika, pouze delegatecall k beaconu.
- Kompatibilita s OZ: implementation() pro upgradeBeaconToAndCall.
- Solidity 0.8.33, Hardhat 3.1.6 — produkčně připravené s testy.
— Editorial Team
Zatím žádné komentáře.