Розробка проксі-контрактів (UUPS, Transparent Proxy)

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

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

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

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

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

Розробка прокси-контрактів (UUPS, Transparent Proxy)

Контракт задеплоєний на mainnet, в ньому знайшли критичну уязвимість. Без прокси — усе, міграція вручну, переконувати користувачів переходити на нову адресу, хоронити старий TVL. З правильно налаштованим прокси — апгрейд через мультисиг, 15 хвилин, адреса контракту не змінюється. Але «правильно налаштованим» — ключова фраза. Прокси-паттерни додають цілий клас уязвимостей, яких немає в immutable-контрактах.

Два паттерни та їх реальні відмінності

Transparent Proxy

Класична реалізація OpenZeppelin (EIP-1967). Proxy-контракт містить логіку маршрутизації: якщо викликає admin — він управляє прокси напряму (upgrade, changeAdmin). Якщо викликає будь-хто інший — виклик делегується в імплементацію.

Проблема: кожен виклик контракту вимагає додаткового SLOAD для читання адресу admin (100 gas по EIP-2929) та порівняння з msg.sender. На гарячих шляхах — постійний overhead. Для протоколу з мільйонами викликів на день — помітно.

Другий момент: ProxyAdmin — окремий контракт, який володіє правами апгрейду. Це додає ще один контракт у систему, ще одну точку відповідальності за ключі.

UUPS (EIP-1822 / ERC-1967)

Логіка апгрейду перенесена з proxy у імплементацію. Proxy сам по собі — «тупий» делегатор без якої-небудь логіки маршрутизації по caller. Немає додаткового SLOAD на кожен виклик — дешевше в експлуатації.

Але є критичний ризик: якщо задеплоїти нову імплементацію без функції upgradeTo (забути унаслідити UUPSUpgradeable, або намисно видалити в цілях економії газу) — прокси назавжди втрачає можливість апгрейду. Контракт заморожений на поточній версії без можливості виправлення.

Реальний кейс: у 2022 році кілька протоколів на UUPS зіткнулися з проблемою «неініціалізованої імплементації». Імплементація деплоїлася без виклику initialize(), й атакуючий викликав initialize() першим, стаючи owner, після чого upgradeTo() дозволяв підмінити імплементацію на самоунищтожуючийся контракт. Усі прокси, що вказували на цю імплементацію, перетворювалися в нерабочі. Рішення: _disableInitializers() у конструкторі імплементації — обов'язковий паттерн у OpenZeppelin 4.3+.

Storage collision — найнебезпечніша проблема

Суть проблеми: proxy та імплементація використовують один storage. Якщо у proxy є змінна в slot 0, а в імплементації також є змінна в slot 0 — вони перезаписують один одного.

EIP-1967 вирішує це для служебних змінних proxy (адреса імплементації, адреса admin) — вони зберігаються у псевдовипадкових слотах на основі keccak256 хеша рядка, практично виключаючи коліізію з користувацьким storage.

Але storage імплементації при апгрейдах — відповідальність розробника. Якщо у V1 була структура:

uint256 public totalSupply;   // slot 0
address public owner;         // slot 1

А у V2 додали змінну перед існуючими:

bool public paused;           // slot 0 — КОЛІІЗІЯ з totalSupply
uint256 public totalSupply;   // slot 1 — КОЛІІЗІЯ з owner
address public owner;         // slot 2

totalSupply тепер читає те, що раніше було owner (адреса, інтерпретована як число). Мовчазна пошкодження даних, без reverting транзакцій, без помилок компілятора.

ERC-7201 (Namespaced Storage Layout) вирішує це радикально. Всі змінні імплементації зібрані в одну struct, яка зберігається в заранее обчисленому слоті:

bytes32 private constant STORAGE_LOCATION = 
    keccak256(abi.encode(uint256(keccak256("myprotocol.storage.v1")) - 1)) & ~bytes32(uint256(0xff));

Нові змінні додаються в кінець struct. Немає коліізій зі служебними слотами proxy, немає проблем при апгрейдах. Це поточна best practice для production UUPS-контрактів.

Інініціалізація замість конструкторів

У прокси-архітектурі конструктор імплементації не виконується у контексті proxy — він виконується тільки при деплої самої імплементації. Тому всі ініціалізуючі дії (встановлення owner, початкові параметри) переносяться у функцію initialize(), захищену модифікатором initializer.

Поширена помилка: initialize() забули викликати після деплоя proxy. Контракт працює, але owner не встановлено — перший, хто викличе initialize(), стане власником. У 2021 році саме так був атакований контракт Parity — неініціалізований WalletLibrary був захоплений, після чого атакуючий викликав kill(), заморозивши 587 ETH назавжди.

Рішення: деплой-скрипт повинен атомарно деплоїти proxy та викликати initialize() у рамках одного скрипту. Ніколи не деплоїти прокси без негайної ініціалізації.

Як ми реалізуємо прокси-контракти

Базова бібліотека — OpenZeppelin Upgrades (Hardhat plugin або Foundry-сумісний варіант). Плагін автоматично перевіряє сумісність storage layout між версіями при кожному апгрейді — це обов'язковий інструмент, не опціональний.

Для UUPS вибираємо UUPSUpgradeable з OpenZeppelin 5.x. Для систем, де апгрейд повинен управлятися DAO або мультисигом — AccessControlUpgradeable з роллю UPGRADER_ROLE, виданою Gnosis Safe адресу.

Тестуємо апгрейди через Foundry fork-тести: форкаємо mainnet, симулюємо апгрейд, перевіряємо, що всі storage-змінні збереглі значення, функції працюють коректно, нові змінні ініціалізовані правильно.

Критерій вибору Transparent Proxy UUPS
Gas на виклик +100-200 gas (SLOAD admin) Без overhead
Ризик втрати upgrade Ні Так (забутий upgradeTo)
Складність коду Нижче Трохи вище
Рекомендація OZ 5.x Застарілий для нових Переважаємий
Окремий ProxyAdmin Так Ні

Коли прокси не потрібен

Immutable-контракт простіший, дешевший в аудиті, викликає більше довіри в користувачів (немає ризику «rug через апгрейд»). Якщо логіка стабільна та ризик критичної помилки мінімальний — прокси додає складність без необхідності.

Для DeFi-протоколів з великим TVL звичайно краще immutable + timelock на параметрах, ніж upgradeability без формального governance.

Процес та строки

Розробка прокси-системи: 2-3 дні на базову реалізацію, ще 1-2 дні на тести апгрейду та storage layout validation. Складні системи з кількома прокси та custom governance — від тижня.

Вартість залежить від кількості контрактів та вимог до governance.