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

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Обновление смарт-контрактов (upgradeable proxy pattern)
Сложная
~2-3 рабочих дня
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1258
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1170
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    873
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1092
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    563
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    830

Обновление смарт-контрактов (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 до передачи права апгрейда
  • Тест: форк mainnet, апгрейд, проверка storage