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

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка прокси-контрактов (UUPS, Transparent Proxy)
Сложная
~2-3 рабочих дня
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

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

Контракт задеплоен на mainnet, в нём нашли критическую уязвимость. Без прокси — всё, миграция вручную, уговаривать пользователей переходить на новый адрес, хоронить старый TVL. С правильно настроенным прокси — апгрейд через мультисиг, 15 минут, адрес контракта не меняется. Но «правильно настроенным» — ключевое словосочетание. Прокси-паттерны добавляют целый класс уязвимостей, которых нет в immutable-контрактах.

Два паттерна и их реальные отличия

Transparent Proxy

Классическая реализация из OpenZeppelin (EIP-1967). Proxy-контракт содержит логику маршрутизации: если вызывает admin — он управляет прокси напрямую (upgrade, changeAdmin). Если вызывает любой другой — вызов делегируется в имплементацию.

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

Второй момент: ProxyAdmin — отдельный контракт, который владеет правами апгрейда. Это добавляет ещё один контракт в систему, ещё одну точку ответственности за ключи.

UUPS (EIP-1822 / ERC-1967)

Логика апгрейда перенесена из proxy в имплементацию. Proxy сам по себе — «тупой» делегатор без какой-либо логики маршрутизации по caller. Нет дополнительного SLOAD на каждый вызов — дешевле в эксплуатации.

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

Реальный кейс: в 2022 году несколько протоколов на 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(), станет владельцем. В 2021 году именно так был атакован контракт Parity — неинициализированный WalletLibrary был захвачен, после чего атакующий вызвал kill(), заморозив 587 ETH навсегда.

Решение: деплой-скрипт должен атомарно деплоить 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.

Процесс и сроки

Разработка прокси-системы: 2-3 дня на базовую имплементацию, ещё 1-2 дня на тесты апгрейда и storage layout validation. Сложные системы с несколькими прокси и custom governance — от недели.

Стоимость зависит от количества контрактов и требований к governance.