Перейти к содержимому
Все статьи
Блокчейн

Апгрейдируемые смарт-контракты: proxy, UUPS, плюсы и риски

·30 октября 2026 г.· 11 мин чтения
Нужен смарт-контракт или dApp?Разработка смарт-контрактов

Апгрейдируемый смарт-контракт — это связка из прокси, который хранит данные и адрес, и контракта реализации с логикой. Прокси перенаправляет каждый вызов в реализацию через delegatecall, поэтому логику можно заменить, а адрес и балансы останутся. Плюс — можно исправить ошибку. Минус — у кого-то появляется право переписать правила, и ошибка в самом механизме обновления может заблокировать или раскрыть все средства. Делать контракт обновляемым стоит только тогда, когда вы готовы защищать это право так же строго, как сами деньги.

Как устроен прокси

delegatecall выполняет код реализации в контексте прокси: читается и пишется storage прокси, msg.sender остаётся исходным пользователем. Адрес реализации хранится в специальной ячейке, описанной в стандарте ERC-1967, чтобы не пересекаться с переменными логики. Обновление — это запись нового адреса в эту ячейку.

Конструктор реализации при этом не выполняется в контексте прокси. Поэтому вместо конструктора пишут функцию initialize с модификатором initializer, которую можно вызвать только один раз. Документация OpenZeppelin подробно описывает это в разделе Upgrades.

Transparent, UUPS и Beacon

СхемаГде логика обновленияПлюсыРиски
TransparentВ прокси и отдельном ProxyAdminОбновление нельзя случайно удалить из логикиДороже деплой и каждый вызов, отдельный контракт администратора
UUPS (ERC-1822)В реализации, функция upgradeToAndCallДешевле, проще прокси, можно навсегда отключить обновленияНовая версия без функции обновления заблокирует апгрейды навсегда
BeaconВ контракте-маяке, общем для многих проксиОбновить сотни одинаковых контрактов одной транзакциейОдин ключ меняет логику сразу везде

В OpenZeppelin 5 при создании TransparentUpgradeableProxy контракт ProxyAdmin разворачивается автоматически, а начиная с версии 5.6 ERC1967Proxy и TransparentUpgradeableProxy требуют данные инициализации прямо при создании. Это закрывает целый класс ошибок, когда прокси оставался неинициализированным и его мог «захватить» первый желающий.

Минимальный UUPS-контракт: что в нём обязательно

Учебный пример на OpenZeppelin Contracts Upgradeable 5.x. Главное здесь не синтаксис, а четыре обязательных элемента.

  • contract BoxV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
  • /// @custom:oz-upgrades-unsafe-allow constructor и constructor() { _disableInitializers(); } — реализацию нельзя инициализировать напрямую.
  • function initialize(address initialOwner) public initializer { __Ownable_init(initialOwner); } — замена конструктора для прокси.
  • function _authorizeUpgrade(address) internal override onlyOwner {} — кто имеет право обновлять.

Что ломается при обновлении

Раскладка storage

Прокси хранит данные по номерам ячеек. Если в новой версии поменять порядок переменных, удалить переменную или вставить новую в середину, старые данные окажутся «под другими именами»: баланс станет адресом владельца, а флаг паузы — суммой. В OpenZeppelin 5 upgradeable-контракты используют пространства имён по ERC-7201, чтобы данные родительских контрактов не пересекались. Ваши собственные переменные всё равно нужно добавлять только в конец или тоже выносить в namespace.

Инициализация

  • Забыли вызвать initialize при создании прокси — права может забрать первый, кто вызовет его.
  • Не вызвали _disableInitializers() в конструкторе реализации — реализацию можно инициализировать и получить над ней контроль.
  • Новая версия требует новых параметров — используйте reinitializer(2), а не повторный initializer.

Несовместимые изменения

  • Ключевое слово immutable в реализации хранится в байт-коде, а не в storage прокси, и это легко упустить.
  • selfdestruct и delegatecall к произвольным адресам в логике опасны для всей связки.
  • Изменение семантики функции при том же названии ломает интеграции, которые на неё полагались.

Инструменты, которые ловят ошибки

Плагины OpenZeppelin Upgrades для Foundry и Hardhat проверяют совместимость раскладки storage между версиями, наличие небезопасных конструкций и правильность инициализации. Запускайте проверку в CI до каждого деплоя новой версии. Тест на форке основной сети, где вы делаете апгрейд и проверяете, что старые балансы на месте, — обязательный минимум. Подробно о тестах — в статье о Foundry и Hardhat.

Кому доверить право обновления

ВариантКогда подходитЧем рискуете
Один ключ разработчикаТолько тестовая сетьПотеря или кража ключа означает потерю контроля над всем
Мультиподпись 2 из 3 или 3 из 5Ранний этап проектаСговор или компрометация нескольких подписантов
Мультиподпись плюс таймлокПродукт с деньгами пользователейЗадержка при срочных исправлениях
Голосование держателей (DAO) плюс таймлокЗрелый протокол с сообществомСложность и захват голосованием
Отказ от обновленийКонтракт стабилен и проверенОшибку уже не исправить

Таймлок — это задержка между решением об обновлении и его исполнением, например несколько дней. Пользователи видят запланированное обновление и могут вывести средства, если не согласны. OpenZeppelin предлагает для этого TimelockController. Как распределять роли — в статье о контроле доступа, а настройку мультиподписи компании мы делаем в рамках решения для казначейства.

Порядок безопасного обновления

  1. Опишите изменения и причину обновления, заранее сообщите о нём пользователям.
  2. Напишите новую версию, сохранив раскладку storage, и прогоните проверку плагином OpenZeppelin Upgrades.
  3. Запустите все старые тесты и добавьте новые на изменённую логику.
  4. Проведите апгрейд на форке основной сети и убедитесь, что балансы, роли и параметры на месте.
  5. Отдайте изменения на ревью, а при существенной логике — на внешний аудит.
  6. Разверните новую реализацию, верифицируйте её код и подготовьте транзакцию обновления в мультиподписи.
  7. Если есть таймлок, дождитесь окончания задержки, исполните обновление и проверьте результат ончейн.
  8. Включите усиленный мониторинг на первые дни после обновления.

Этот порядок кажется долгим, но каждое звено закрывает конкретный сценарий потери средств. В публичных разборах инцидентов с обновляемыми контрактами причиной нередко оказывается не новая логика, а пропущенная инициализация или сдвинутая переменная. Настройку оповещений о вызовах upgradeToAndCall и смене администратора мы делаем в рамках мониторинга контрактов.

Что видят пользователи

Каждое обновление оставляет след: событие Upgraded с адресом новой реализации записывается в лог прокси, а обозреватели показывают историю реализаций. Внимательные держатели и аналитики следят за этим. Поэтому лучше заранее опубликовать, кто владеет правом обновления, какая задержка у таймлока и где читать описание каждой версии. Прозрачность здесь работает на доверие сильнее любых обещаний в документации.

Когда обновляемость не нужна

Простой токен с фиксированной эмиссией, NFT-коллекция без сложной логики, контракт вестинга с заранее известным графиком — всё это лучше делать неизменяемым. Держатели доверяют такому контракту больше, а код проще проверить. Если ошибку всё же нашли, план Б — новый контракт и миграция, и его стоит продумать заранее.

Если вы унаследовали обновляемый контракт и не уверены, кто сейчас может его обновить, это первое, что нужно выяснить. С проверкой поможет аудит смарт-контракта, а с разработкой новой версии — команда разработки смарт-контрактов.

Вопросы и ответы

Что такое прокси-контракт простыми словами?

Это контракт-посредник с постоянным адресом, который хранит данные и пересылает вызовы в отдельный контракт с логикой. Логику можно заменить, а адрес и балансы останутся прежними.

Что выбрать: UUPS или Transparent?

UUPS дешевле и проще, его рекомендует OpenZeppelin для новых проектов, но требует аккуратности: каждая новая версия должна сохранять функцию обновления. Transparent надёжнее в этом смысле, но дороже по газу.

Можно ли сделать контракт обновляемым после деплоя?

Нет. Если контракт развёрнут без прокси, его логику не заменить. Можно развернуть новую версию за прокси и перенести в неё средства и данные.

Что такое _disableInitializers?

Это функция OpenZeppelin, которую вызывают в конструкторе реализации. Она запрещает инициализировать саму реализацию напрямую, чтобы посторонний не получил над ней права владельца.

Как пользователю проверить, обновляемый ли контракт?

Обозреватели вроде Etherscan показывают пометку прокси и адрес реализации. Стоит также посмотреть, кто владеет правом обновления: один адрес, мультиподпись или таймлок.

Нужна помощь с проектом?

Обсудим задачу и предложим решение — от сайта до SaaS и защиты.

Связаться