Powrót do strony głównej

NamedBeacon Proxy: optymalizacja Solidity proxy

NamedBeacon i NamedBeaconProxy oferują efektywną alternatywę dla standardowej BeaconProxy. Scentralizowane przechowywanie implementacji po bytes32 ID minimalizuje rozmiar proxy i duplikowanie kodu. Nadaje się do production z Solidity 0.8.33.

Efektywne NamedBeacon i Proxy w Solidity bez zbędnego kodu
Advertisement 728x90

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:

Google AdInline article slot
  • 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:

Google AdInline article slot
// 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).

Google AdInline article slot

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

Advertisement 728x90

Czytaj dalej