Разработка смарт-контрактов с Minimal Proxy (EIP-1167 Clone)

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка смарт-контрактов с Minimal Proxy (EIP-1167 Clone)
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

Разработка смарт-контрактов с Minimal Proxy (EIP-1167 Clone)

Деплой одного контракта стейкинга стоит 0.05 ETH при цене газа 30 gwei. Если вам нужно создать 1000 экземпляров этого контракта для 1000 пользователей — это 50 ETH на деплой. EIP-1167 Minimal Proxy сокращает это до 0.003 ETH за экземпляр: суммарно 3 ETH вместо 50.

Паттерн используют: Uniswap V2 (пара создаётся как клон фабрики), Gnosis Safe (каждый кошелёк — клон), большинство современных NFT-фабрик.

Что такое Minimal Proxy

EIP-1167 определяет стандартный bytecode размером 45 байт, который является прокси к реализации. Весь bytecode — это просто DELEGATECALL к адресу реализации. Никакой логики, никакого storage — только делегирование.

Bytecode proxy в hex:

3d602d80600a3d3981f3363d3d373d3d3d363d73<implementation_address>5af43d82803e903d91602b57fd5bf3

Где <implementation_address> — 20-байтовый адрес реализации, вшитый в bytecode. Именно потому контракт такой дешёвый: там буквально 45 байт bytecode.

DELEGATECALL означает, что код реализации выполняется в контексте proxy: msg.sender и msg.value сохраняются, address(this) — адрес proxy, storage записывается в proxy. Реализация не имеет своего storage, только код.

OpenZeppelin Clones

OpenZeppelin предоставляет библиотеку Clones для работы с EIP-1167:

import "@openzeppelin/contracts/proxy/Clones.sol";

contract StakingFactory {
    address public immutable implementation;
    
    event StakingDeployed(address indexed clone, address indexed owner);
    
    constructor(address _implementation) {
        implementation = _implementation;
    }
    
    function deployStaking(
        address rewardToken,
        uint256 rewardRate,
        address owner
    ) external returns (address clone) {
        // Детерминированный deploy через CREATE2
        bytes32 salt = keccak256(abi.encodePacked(owner, rewardToken, block.timestamp));
        clone = Clones.cloneDeterministic(implementation, salt);
        
        // Инициализация (вместо конструктора — функция initialize)
        IStaking(clone).initialize(rewardToken, rewardRate, owner);
        
        emit StakingDeployed(clone, owner);
    }
    
    // Предсказать адрес до деплоя
    function predictAddress(
        address owner,
        address rewardToken,
        uint256 timestamp
    ) external view returns (address) {
        bytes32 salt = keccak256(abi.encodePacked(owner, rewardToken, timestamp));
        return Clones.predictDeterministicAddress(implementation, salt);
    }
}

Отличие clone от cloneDeterministic

Clones.clone() использует CREATE opcode — адрес зависит от nonce фабрики. Clones.cloneDeterministic() использует CREATE2 — адрес определяется salt и адресом фабрики. CREATE2 предпочтительнее: адрес можно вычислить off-chain до деплоя, что важно для UI и для предварительного одобрения (approve до деплоя контракта).

Критический момент: initializer вместо constructor

Конструктор реализации выполняется один раз при деплое реализации, не при деплое клонов. Клоны получают чистый storage. Поэтому реализация использует паттерн initializer:

contract StakingImplementation {
    address public rewardToken;
    uint256 public rewardRate;
    address public owner;
    bool private _initialized;
    
    // Защита от повторной инициализации
    modifier initializer() {
        require(!_initialized, "Already initialized");
        _initialized = true;
        _;
    }
    
    function initialize(
        address _rewardToken,
        uint256 _rewardRate,
        address _owner
    ) external initializer {
        rewardToken = _rewardToken;
        rewardRate = _rewardRate;
        owner = _owner;
    }
    
    // Бизнес-логика...
}

OpenZeppelin предоставляет Initializable базовый контракт с более robust защитой. Используем его, не пишем свой:

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract StakingImplementation is Initializable {
    function initialize(...) external initializer {
        // ...
    }
}

Уязвимость: неинициализированная реализация

Реализация тоже является контрактом с публичной функцией initialize(). Если её не вызвать на самой реализации — кто угодно может вызвать initialize() на реализации с произвольными параметрами и стать owner. Это не ломает клоны (у них свой storage), но является проблемой если реализация имеет какую-то функциональность или хранит state.

Решение: вызвать _disableInitializers() в конструкторе реализации:

constructor() {
    _disableInitializers(); // OpenZeppelin Initializable
}

Это деактивирует функцию initialize() на самом контракте реализации, оставляя её рабочей только через DELEGATECALL из клонов.

Сравнение с другими proxy паттернами

Паттерн Gas на deploy Апгрейдность Сложность Использование
EIP-1167 Clone ~40k gas Нет Низкая Много экземпляров одной логики
Transparent Proxy ~400k gas Да Средняя Апгрейдный одиночный контракт
UUPS ~300k gas Да Средняя Апгрейдный, логика в реализации
Beacon Proxy ~200k gas Да (все сразу) Высокая Много апгрейдных экземпляров
Diamond (EIP-2535) ~500k gas Да (по facets) Высокая Сложная модульная логика

Клоны не апгрейдаемы по определению: bytecode вшит в proxy. Если нужна апгрейдность для множества экземпляров — Beacon Proxy: все клоны смотрят на beacon-контракт, который хранит адрес реализации. Меняем адрес в beacon — обновляются все.

Типичные ошибки

Storage collision при изменении реализации. Если деплоить новую реализацию с другим storage layout — старые клоны читают storage по старым offset-ам, но новая реализация интерпретирует их иначе. Без апгрейдности это не проблема: реализация фиксирована. Но если вы решаете добавить апгрейдность через Beacon post-factum — нужен storage gap.

Не передавать ETH в initialize(). Если конструктор реализации принимал ETH — initializer должен тоже. Но в большинстве случаев инициализация не требует ETH — не усложняем.

Использование immutable переменных в реализации. Immutable хранится в bytecode реализации, а не в storage. При DELEGATECALL клон читает storage из себя, но код — из реализации. Immutable работает корректно: клон читает значение из bytecode реализации. Это нормальное поведение, но нужно понимать.

Разработка фабрики с Minimal Proxy: 2-3 рабочих дня. Стоимость рассчитывается индивидуально.