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

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка смарт-контрактів з Minimal Proxy (EIP-1167 Clone)
Середній
~2-3 дні
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1285
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1198
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    902
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1119
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    587
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    853

Розробка смарт-контрактів з 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 визначає стандартний байткод розміром 45 байт, який є проксі до реалізації. Весь байткод — це просто DELEGATECALL на адресу реалізації. Ніякої логіки, ніякого сховища — тільки делегування.

Байткод проксі у hex:

3d602d80600a3d3981f3363d3d373d3d3d363d73<implementation_address>5af43d82803e903d91602b57fd5bf3

Де <implementation_address> — 20-байтова адреса реалізації, закодована в байткоді. Саме тому контракт такий дешевий: буквально 45 байт байткода.

DELEGATECALL означає, що код реалізації виконується в контексті проксі: msg.sender та msg.value зберігаються, address(this) — адреса проксі, сховище записується у проксі. Реалізація не має свого сховища, тільки код.

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) {
        // Детерміністичне розгортання через 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

Конструктор реалізації виконується один раз при розгортанні реалізації, а не при розгортанні клонів. Клони отримують чисте сховище. Тому реалізація використовує паттерн 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 з більш надійним захистом. Використовуйте його, не пишіть свій:

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

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

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

Реалізація також є контрактом з публічною функцією initialize(). Якщо ви не викликатимете її самої реалізації — будь-хто може викликати initialize() на реалізації з довільними параметрами та стати власником. Це не ломає клони (у них своє сховище), але є проблемою, якщо реалізація має якусь функціональність або зберігає стан.

Рішення: викликайте _disableInitializers() у конструкторі реалізації:

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

Це деактивує initialize() на самому контракті реалізації, залишаючи його робочим тільки через DELEGATECALL з клонів.

Порівняння з іншими паттернами проксі

Паттерн Газ на розгортання Можливість оновлення Складність Випадок використання
EIP-1167 Clone ~40k gas Ні Низька Багато екземплярів однієї логіки
Transparent Proxy ~400k gas Так Середня Один оновлюваний контракт
UUPS ~300k gas Так Середня Оновлюваний, логіка в реалізації
Beacon Proxy ~200k gas Так (всі разом) Висока Багато оновлюваних екземплярів
Diamond (EIP-2535) ~500k gas Так (за facets) Висока Складна модульна логіка

Клони не оновлюються за визначенням: байткод закодований у проксі. Якщо вам потрібна можливість оновлення для багатьох екземплярів — використовуйте Beacon Proxy: всі клони вказують на beacon контракт, який зберігає адресу реалізації. Змініть адресу у beacon — оновляться всі.

Типові помилки

Конфлікт сховища при зміні реалізації. Якщо розгорнути нову реалізацію з іншим макетом сховища — старі клони читають сховище за старими зміщеннями, але нова реалізація їх інтерпретує інакше. Без можливості оновлення це не проблема: реалізація фіксована. Але якщо ви вирішите добавити можливість оновлення через Beacon post-factum — вам потрібен storage gap.

Не передавайте ETH у initialize(). Якщо конструктор реалізації приймав ETH — initializer повинен теж. Але у більшості випадків ініціалізація не вимагає ETH — не ускладнюйте.

Використання immutable змінних у реалізації. Immutable зберігається в байткоді реалізації, а не у сховищі. Під час DELEGATECALL клон читає сховище з себе, але код — з реалізації. Immutable працює коректно: клон читає значення з байткода реалізації. Це нормальна поведінка, але потрібно це розуміти.

Розробка фабрики з Minimal Proxy: 2-3 робочі дні. Вартість розраховується індивідуально.