Оновлення смарт-контрактів (upgradeable proxy pattern)

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Оновлення смарт-контрактів (upgradeable proxy pattern)
Складний
~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

Оновлення смарт-контрактів (upgradeable proxy pattern)

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

Storage collision — найнебезпечніша помилка при реалізації прокси

Класичний proxy працює через DELEGATECALL: proxy-контракт викликає implementation, але виконує код у контексті хранилища proxy. Storage в EVM — це масив з 2²⁵⁶ слотів по 32 байта. Proxy зберігає адресу implementation у слоті 0. Implementation зберігає, наприклад, owner у слоті 0.

Результат — storage collision: owner в implementation перезаписує адресу implementation в proxy. Якщо атакуючий може викликати функцію, яка модифікує owner в implementation, він отримує контроль над proxy.

EIP-1967 вирішує це радикально: зберігає адресу implementation у псевдовипадковому слоті, обчисленому як keccak256("eip1967.proxy.implementation") - 1. Вірогідність колізії з користувацькими змінними implementation — астрономічно мала. OpenZeppelin ERC1967Proxy реалізує саме цей стандарт.

Три основні паттерни

Transparent Proxy (TUP). Класика OpenZeppelin. Два типи вихідних: admin (керує upgrade) та користувачі (викликають логіку). Admin не може викликати функції implementation — тільки апгрейдити. Користувачі не можуть викликати admin-функції. Реалізується через ifAdmin modifier у proxy. Overhead на кожний виклик — одне додаткове чтення storage для перевірки msg.sender.

UUPS (EIP-1822). Логіка апгрейду перенесена в сам implementation-контракт. Proxy став тоньше — менше gas на виклик. Але тут критична пастка: якщо розгорнути новий implementation без функції upgradeTo, контракт навсегда втратить можливість апгрейда. OpenZeppelin UUPSUpgradeable додає перевірку _authorizeUpgrade — це єдиний захист.

Beacon Proxy. Один beacon-контракт зберігає адресу implementation. Множество proxy-контрактів дивляться в цей beacon. Один апгрейд beacon'а — оновлюються всі proxy одночасно. Ідеально для фабрик (factory pattern), де потрібно створювати багато однакових контрактів (наприклад, пули в AMM).

Паттерн Gas на виклик Гнучкість Ризики
Transparent +2100 gas (SLOAD) Висока Storage collision при неправильному layout
UUPS Мінімальний Висока Втрата upgradability при помилці
Beacon Середній Максимальна для фабрик Одна точка відмови (beacon)

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

constructor() у Solidity виконується один раз при деплое. При proxy-паттерні implementation розгортається окремо — його конструктор виконується у контексті implementation, а не proxy. Всі змінні, встановлені у конструкторі, залишаються в implementation та недоступні через proxy.

Рішення: замінити конструктор на функцію initialize() з модифікатором initializer з OpenZeppelin. Вона викликається один раз через proxy та записує дані у storage proxy.

Типова помилка — забути викликати _disableInitializers() у конструкторі implementation. Без цього атакуючий може викликати initialize() прямо на implementation (не через proxy) та стати його owner. Це не впливає на proxy прямо, але відкриває вектори для атаки через DELEGATECALL.

Як реалізуємо апгрейд

Вся розроблення — через Hardhat + OpenZeppelin Upgrades Plugin або Foundry з кастомними скриптами.

OpenZeppelin Upgrades Plugin перевіряє storage layout автоматично: якщо новий implementation порушує layout попереднього (додає змінну перед існуючими, а не після), плагін вибросить помилку до деплоя. Це критично важливо — порушення layout виявляється не на тестах, а в продакшені, коли balanceOf починає повертати мусор.

Приклад робочого флоу:

  1. npx hardhat run scripts/deploy-proxy.ts — деплой proxy + implementation + ProxyAdmin
  2. npx hardhat run scripts/upgrade.ts — деплой нового implementation, виклик upgrade() на ProxyAdmin
  3. Перевірка: getImplementation() повертає нову адресу, storage proxy не змінився

Для Foundry використовуємо кастомний скрипт з vm.startBroadcast(), явним збереженням адрес у JSON-файл (deployments/{chainId}.json) та верифікацією на Etherscan через forge verify-contract.

Timelock та мультисиг на апгрейд

Право апгрейду контракту — це право змінити правила гри для всіх користувачів. У DeFi-протоколах з TVL вище $1M передавати це право одному EOA (Externally Owned Account) неприйнятно.

Стандартна схема: Gnosis Safe (мультисиг 3-of-5) як owner ProxyAdmin + TimelockController з затримкою 48-72 години. Апгрейд проходить три етапи: propose → очікування timelock → execute. Користувачі бачать pending upgrade та мають час вивести кошти.

OpenZeppelin Governor + TimelockController — для протоколів з DAO-управлінням: upgrade проходить через голосування токенхолдерів.

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

Аудит поточного контракту. Якщо контракт вже в продакшені та потрібно додати upgradability — це складніше, ніж спроектувати з нуля. Дивимось storage layout, виявляємо що потрібно зберегти.

Вибір паттерна. UUPS для одиночних контрактів, Beacon для фабрик, Transparent для legacy-проектів з великою командою.

Тестування апгрейду. Hardhat тест: деплой V1, запис стану, апгрейд до V2, перевірка що стан збережений. Foundry: fork mainnet-стани через vm.createFork, прогон upgrade-скрипту на форці.

Деплой через мультисиг. Всі production-деплої — через Safe Transaction Builder. Ніяких private key у скриптах.

Часові рамки

Реалізація proxy-паттерна для нового контракту — 2-3 робочих дні. Міграція існуючого непрокси-контракту на upgradeable-архітектуру (з збереженням даних через migration script) — від 3 до 7 днів залежно від складності storage.

Вартість розраховується індивідуально.

Чек-лист перед деплоем upgradeable-контракту

  • _disableInitializers() викликано в конструкторі implementation
  • initialize() захищено модифікатором initializer
  • Storage layout перевірено через OpenZeppelin Upgrades Plugin (validate)
  • ProxyAdmin owner — мультисиг, не EOA
  • Timelock налаштовано для production
  • Новий implementation верифіковано на Etherscan до передачі права апгрейду
  • Тест: fork mainnet, апгрейд, перевірка storage