Обновление смарт-контрактов (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 начинает возвращать мусор.
Пример рабочего флоу:
-
npx hardhat run scripts/deploy-proxy.ts— деплой proxy + implementation + ProxyAdmin -
npx hardhat run scripts/upgrade.ts— деплой нового implementation, вызовupgrade()на ProxyAdmin - Проверка:
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







