Привет,

Время от времени мою светлую голову озаряют "элегантные решения сложнейших задач", которые почему-то никем не были решены до меня (*сарказм*), и сейчас я поделюсь с вами очередной такой киллер идеей на триллион копеек. Я назвал её "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