Привет,

Время от времени мою светлую голову озаряют "элегантные решения сложнейших задач", которые почему-то никем не были решены до меня (*сарказм*), и сейчас я поделюсь с вами очередной такой киллер идеей на триллион копеек. Я назвал её "NamedBeacon and Proxy".

Собственно, речь о прокси (proxy) и беконах (beacon - "маяк") для обновляемых смартов на Solidity. Все началось с неудовлетворенности реализацией BeaconProxy от OpenZeppelin:

  • используемый бекон хранит лишь 1 адрес имплементации (имеет только 1 функцию implementation() external view returns (address), следовательно, чтобы хранить аж целый 1 слот адреса надо деплоить отдельный смарт;

  • реализация BeaconProxy к тому же хранит адрес бекона два раза - в immutable private переменной и еще в ERC1967Utils;

  • так же в прокси хранится много кода для администрирования (бекон, имплементация, админ) - каждый раз деплоя прокси, вы деплоите кучу всего лишнего.

Мое гениальное решение состоит из двух смартов, работающих в паре:

  1. NamedBeacon:

    1. имеет 2 основные функции для регистрации и чтения адресов имплементаций по айди строке

      1. registerImplementation(bytes32 referenceId, address implementation) external

      2. getImplementation(bytes32 referenceId) external view returns (address implementation)

    2. администрирование ведется через этот смарт - логика администрирования в одном месте

    3. минималистичен (время от времени я добавляю/удаляю в него проверки правильности ввода, чтения списка зарегистрированных зависимостей, но в целом это не необходимо - в основном достаточно просто сохранить референс с айди на адрес)

    4. недостаток - текущая версия подразумевает, что контракт 1 раз задеплоится и больше не будет изменяться (ну, типа, зачем?) (я имею в виду, что храню данные в storage, а не по SlotStorage паттерну)

  2. NamedBeaconProxy

    1. хранит ссылку на бекон, а так же реф айди, по которому будет читать адрес имплементации в immutable - задается в конструкторе, не влияет на storage

    2. часть проверок скопирована из ERC1967Utils и BeaconProxy, но так-то их можно и скипнуть (например, можно убрать проверки на прочитанный из бекона адрес - следовательно, сохранить еще немного газа на вызовах этого прокси).

Как это работает:

  • деплоится бекон

  • деплоятся имплементации

  • имплементации регистрируются в беконе

  • под имплементацию деплоится прокси с указанием бекона и айди для чтения имплементации

  • всё.

Код можно посмотреть на гитхаб (понравится - smash the star button! pzhl).
Hardhat 3.1.6, Solidity 0.8.33, имплементация, скрипты, пару тестов, всё как в проде (ну почти).

(чёт как‑то немного получилось, поэтому ниже сокращенный код. код с комментами, ивентами и прочим - в гитхабе)

// 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 imlpId => address imlpAddress) 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);
    }
}
// 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();
        }
    }
}

---
С 2009го пишу код, лидю тимы, доходил до пресейл техлида и руководителя отдела разработки.
Рассматриваю новые предложения (писать код, и/или пинать молодые горячие головы, и/или строить архитектуры, и/или говорить с заказчиком).

C#, JavaScript, Solidity, English C1; учу Rust. Посмотреть на фоточку меня можно на GH

Комментарии (2)


  1. 0xInnominatus
    26.03.2026 17:09

    Выглядит интересно, но так как это библиотека, то есть вопрос: почему жёстко фиксируется solidity версии 0.8.33? В коде не увидел использования никаких фичей из новых версий, однако столь высокая версия ограничивает использование на многих сетях, где ещё не ввели новые опкоды (например, Moonbeam, Merlinchain etc). ИМХО при разработке библиотек лучше всего всегда указывать минимально возможную версию solidity с прагмой >=, чтобы всё компилировалось в как можно большем числе проектов на максимально возможном числе сетей.


    1. tema_rebel Автор
      26.03.2026 17:09

      с одной стороны, этот код я скопировал из своего другого проекта, где использую transient, а это как раз 0.8.33, емпин; (там можно было так жестко указать версию, без последствий, да и я предпочитаю жестко задавать версии, чтобы не было потом "случайных" обновлений)
      с другой стороны, я так-то забил на эту деталь... Вообще, хороший поинт, я пересмотрю код. Думаю, 0.8.0 должно хватить (из breaking changes после 0.8.0 я помню только `assembly("memory-safe")` в 0.8.13, но это не используется в данной библиотечке)