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

Розробка проксі-контрактів (UUPS, Transparent Proxy) Контракт задеплоєно на mainnet, у ньому знайдено критичну вразливість. Без проксі — все, міграція вручну, вмовляти користувачів переходити на нову адресу, ховати старий TVL. Ми розуміємо: з правильно налаштованим проксі — апгрейд через мультисі

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

Часті запитання

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

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

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

Контракт задеплоєно на mainnet, у ньому знайдено критичну вразливість. Без проксі — все, міграція вручну, вмовляти користувачів переходити на нову адресу, ховати старий TVL. Ми розуміємо: з правильно налаштованим проксі — апгрейд через мультисіг за 15 хвилин, адреса контракту не змінюється. Але «правильно налаштованим» — ключове словосполучення. Проксі-патерни додають цілий клас вразливостей, яких немає в immutable-контрактах: storage collision, неініціалізовані імплементації, втрата права апгрейда. Наша команда за 7 років реалізувала десятки проксі-систем для DeFi-протоколів з TVL до $500M. Вибір патерну — не технічна формальність, а архітектурне рішення, що впливає на gas, безпеку та управління. Нижче розберемо два основні патерни — Transparent Proxy і UUPS, їх компроміси та типові помилки, а також як ми проектуємо і тестуємо такі системи.

Проблеми, які вирішуємо

Головне завдання — дати можливість виправляти помилки та додавати фічі без міграції користувачів. Але за це доводиться платити:

  • Storage collision: при апгрейді змінні можуть перезаписатися — мовчазне псування даних.
  • Неініціалізована імплементація: якщо забути викликати initialize(), зловмисник може захопити контракт.
  • Втрата права апгрейда: імплементація без upgradeTo назавжди блокує оновлення.
  • Gas overhead: Transparent Proxy додає SLOAD на кожен виклик, що критично для високонавантажених протоколів. Наприклад, при 10 000 транзакцій на день додатковий витрата газу становить 1 000 000 gas ($20 за поточними цінами).

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

Transparent Proxy

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

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

Чому UUPS сьогодні — стандарт індустрії?

Логіка апгрейда перенесена з proxy в імплементацію. Proxy сам по собі — тупий делегатор без будь-якої логіки маршрутизації по caller. Немає додаткового SLOAD на кожен виклик — дешевший в експлуатації. OpenZeppelin починаючи з версії 4.x рекомендує UUPS для нових проектів, що підтверджено в офіційному репозиторії.

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

Реальний кейс: кілька протоколів на 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(), стане власником. В результаті інциденту з контрактом WalletLibrary було втрачено 587 ETH (на момент атаки ~$1.5M) через захоплення неініціалізованого контракту. Рішення: деплой-скрипт повинен атомарно деплоїти 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. Однак, якщо ви передбачаєте необхідність оновлень — проксі незамінний.

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

  1. Аналітика вимог — 1-2 дні. Вибір патерну, визначення ролей і таймлоків.
  2. Проектування storage layout — 1 день. Схема з ERC-7201, перевірка на колізії.
  3. Розробка імплементацій — 2-3 дні. Solidity код з тестами на Foundry (fork).
  4. Розгортання та ініціалізація — 1 день. Атомарний деплой через скрипт.
  5. Тестування апгрейда — 1 день. Перевірка сумісності зберігання.
  6. Внутрішній аудит — 2-4 дні. Включає Slither, Mythril та ручний review.

Загальні строки: від 5 робочих днів для простого проксі до 2-3 тижнів для складних систем з governance та кількома імплементаціями. Зв'яжіться з нами для точної оцінки вашого проекту — ми розрахуємо строки під ключ.

Чек-лист для безпечного деплою проксі
  • [ ] Вибраний патерн (UUPS / Transparent) з обґрунтуванням.
  • [ ] Storage layout реалізовано через ERC-7201.
  • [ ] В конструкторі імплементації викликано _disableInitializers().
  • [ ] Деплой-скрипт викликає initialize() атомарно.
  • [ ] ProxyAdmin (якщо Transparent) розгорнуто та налаштовано.
  • [ ] Роль UPGRADER_ROLE призначена мультисігу.
  • [ ] Storage сумісність перевірено плагіном OpenZeppelin.
  • [ ] Форк-тести симулюють апгрейд зі збереженням даних.
  • [ ] Аудит проведено (внутрішній або зовнішній).
  • [ ] Документація процедури апгрейда підготовлена.

Гарантія та досвід

Ми розробили понад 50 проксі-систем для DeFi, NFT та інфраструктурних проектів. 7+ років практичного досвіду в Solidity та Ethereum. Кожен проект супроводжуємо аудитом та надаємо гарантію коректності апгрейда на тестнеті. Отримайте консультацію з вибору патерну та архітектури проксі для вашого проекту — це безкоштовно.