Обучение

Transparent, UUPS и Beacon Proxy: чем отличаются обновляемые контракты

Сравнение Transparent, UUPS и Beacon Proxy: где хранится implementation, кто выполняет upgrade, каков радиус обновления и какие права и storage risks проверять.

Transparent, UUPS и Beacon Proxy: чем отличаются обновляемые контракты

Upgradeable proxy позволяет менять логику смарт-контракта, сохраняя его адрес, storage и баланс. Для пользователя это означает, что проверенный сегодня код не обязательно останется тем же завтра. Но слово proxy ещё не объясняет, кто имеет право на обновление и сколько контрактов изменятся одной транзакцией.

Transparent, UUPS и Beacon Proxy используют `delegatecall`, однако размещают upgrade-механику по-разному. У Transparent она находится в proxy и связанном ProxyAdmin, у UUPS – в implementation, а Beacon выносит адрес общей implementation в отдельный beacon. Разница влияет на стоимость развёртывания, управление, возможность заморозить обновления и радиус ошибки.

Базовая модель proxy и implementation

Пользователь отправляет вызов на постоянный proxy address. Proxy находит implementation и выполняет её код через `delegatecall`. Код берётся из implementation, но `msg.sender`, баланс и storage остаются контекстом proxy. Поэтому два proxy могут использовать одну логику и хранить независимые балансы.

Implementation нельзя воспринимать как обычный второй контракт, куда пользователь переводит активы. Значимые данные обычно лежат по адресу proxy. Если вызвать implementation напрямую, операция затронет её собственный storage и может иметь совсем другой результат.

Конструктор implementation также не инициализирует proxy storage. Upgradeable contracts используют `initialize` и последующие `reinitialize`. Если инициализатор proxy остался доступным, посторонний адрес способен получить роль owner или admin. Если implementation не заблокирована от инициализации, атакующий иногда захватывает её собственное состояние и использует опасные функции.

Зачем нужен ERC-1967

При `delegatecall` implementation записывает переменные в storage proxy. Если proxy хранит свой implementation в обычном slot, логика приложения может случайно перезаписать этот адрес. ERC-1967 задаёт специальные slots, выбранные за пределами стандартной раскладки Solidity.

Стандарт определяет отдельные slots для implementation, admin и beacon. Обозреватель может прочитать их без публичных getter-функций и показать ABI текущей логики. Изменения рекомендуется сопровождать событиями `Upgraded`, `AdminChanged` и `BeaconUpgraded`.

Slot решает конфликт между служебным адресом proxy и переменными приложения, но не делает обновление безопасным автоматически. Новая implementation всё равно должна сохранять совместимую storage layout, корректно ограничивать upgrade и правильно инициализировать новые поля.

Как работает Transparent Proxy

Transparent pattern разделяет обычного пользователя и администратора по `msg.sender`. Вызов не от admin делегируется implementation, даже если selector совпадает с upgrade-функцией proxy. Admin, наоборот, может обновлять proxy, но не должен использовать его как обычный пользователь implementation.

В OpenZeppelin управление обычно передано отдельному ProxyAdmin. ProxyAdmin вызывает `upgradeToAndCall`, а owner ProxyAdmin определяет, кто может инициировать обновление. Поэтому в аудите недостаточно увидеть адрес admin slot – нужно открыть ProxyAdmin и установить его фактического owner.

Детали зависят от версии библиотеки. В OpenZeppelin Contracts 5.x TransparentUpgradeableProxy создаёт связанный ProxyAdmin, а административный адрес внутри proxy задаётся при развёртывании как immutable. ERC-1967 admin slot при этом остаётся видимым для совместимости, но доверять одному slot без проверки bytecode и версии нельзя.

Преимущество Transparent – явное разделение ролей и отсутствие upgrade-функции в бизнес-логике. Недостатки – более тяжёлый proxy, отдельный административный контракт и риск ошибочно использовать admin account для пользовательских вызовов.

Как работает UUPS Proxy

UUPS использует минимальный ERC1967Proxy. Сам proxy знает implementation slot и выполняет делегирование, но не содержит полноценной внешней политики обновления. Функция `upgradeToAndCall` и проверка прав находятся в implementation через UUPSUpgradeable.

Разработчик обязан реализовать `_authorizeUpgrade`. В ней может стоять `onlyOwner`, AccessControl, timelock или проверка governance executor. Когда функция вызывается через proxy, код implementation записывает новый адрес в ERC-1967 slot самого proxy.

Совместимая implementation отвечает на `proxiableUUID()`. Защитная проверка снижает риск обновления на код, который не умеет работать с ожидаемым slot. При этом авторизация остаётся проектной логикой: ошибка в `_authorizeUpgrade` способна открыть обновление любому адресу.

UUPS дешевле разворачивать многократно, потому что upgrade-код находится в общей implementation. Его можно намеренно удалить в будущей версии и тем самым заморозить proxy. Но случайное удаление или несовместимый upgrade может навсегда лишить систему возможности исправления.

Как работает Beacon Proxy

Beacon Proxy не выбирает implementation напрямую. Он обращается к beacon contract, а тот возвращает текущий implementation address через `implementation()`. Owner upgradeable beacon меняет этот адрес одной транзакцией.

Главное свойство – один beacon обслуживает много proxy. У каждого proxy собственный storage и баланс, но все получают одну версию логики. Это удобно для factory, которая создаёт множество однотипных vault, account или market contracts.

Та же связь создаёт большой blast radius. Ошибка в новой implementation одновременно затрагивает все proxy, направленные на beacon. Компрометация owner beacon даёт атакующему путь к массовой замене логики, а не к одному экземпляру.

Чтобы установить текущий код, нужно сначала найти beacon, затем вызвать у него `implementation()`. Чтение только ERC-1967 implementation slot proxy может дать пустое значение и привести к неверному выводу, что proxy не обновляемый.

Ключевые различия

  • Transparent. Upgrade-interface находится в proxy, а право обычно проходит через owner отдельного ProxyAdmin.
  • UUPS. Upgrade-interface и авторизация находятся в implementation, а адрес логики хранится непосредственно в каждом proxy.
  • Beacon. Proxy хранит ссылку на beacon, beacon хранит общую implementation, а его owner обновляет сразу группу экземпляров.
  • Радиус обновления. Transparent и UUPS обычно обновляются по одному proxy. Beacon меняет все связанные proxy одновременно.
  • Стоимость. UUPS переносит больше кода в общую implementation. Transparent несёт административную логику в proxy. Beacon экономит управление большой серией экземпляров.
  • Заморозка. UUPS может удалить upgrade-механику новой implementation. Transparent остаётся обновляемым, пока работает proxy admin path. Beacon зависит от возможности обновить сам beacon.

Как определить паттерн onchain

Начните с proxy address, а не с токена или сайта. Проверьте bytecode и пометку обозревателя. Затем прочитайте ERC-1967 implementation, beacon и admin slots на одном и том же block tag. Пошаговый практический порядок описан в статье о проверке proxy admin и implementation.

Если implementation slot заполнен, откройте этот адрес и изучите verified source. Наличие UUPSUpgradeable, `proxiableUUID`, `upgradeToAndCall` и `_authorizeUpgrade` указывает на UUPS. Но selector сам по себе не доказательство – кастомный контракт может использовать похожие имена.

Для Transparent найдите ProxyAdmin и его owner. Учитывайте версию OpenZeppelin и immutable admin в новых реализациях. Попытка вызвать proxy admin-функцию от обычного адреса не всегда покажет административный интерфейс, потому что transparent dispatch специально разделяет callers.

Если заполнен beacon slot или bytecode относится к BeaconProxy, откройте beacon и вызовите `implementation()`. Затем проверьте owner beacon и все механизмы над ним: multisig, timelock, governance или другой proxy.

Кто на самом деле контролирует upgrade

Адрес с ролью upgrade authority может быть EOA, multisig, timelock или governance executor. Label «multisig» недостаточен: нужно прочитать owners, threshold, modules и guard. Практический чек-лист приведён в статье о проверке владельцев multisig.

Если owner – timelock, проверьте минимальную задержку, proposer, executor, canceller и возможность изменить саму задержку. Номинальная пауза бесполезна, если admin может обойти очередь или немедленно выдать себе роль.

Если upgrade проходит через governance, выясните proposal threshold, quorum, voting delay, voting period и execution delay. DAO frontend не доказывает, что именно этот executor записывает ERC-1967 slot.

Цепочка контроля может быть многоуровневой: proxy управляет ProxyAdmin, ProxyAdmin принадлежит Safe, Safe имеет module, а module обновляется другим proxy. Аудит заканчивается только после раскрытия каждого звена до понятных ключей и правил.

Storage layout – общий риск всех паттернов

Proxy сохраняет старый storage, поэтому новая implementation должна интерпретировать slots так же. Нельзя без миграции поменять порядок существующих переменных, удалить поле и занять его место другим типом или изменить наследование так, чтобы сдвинулась layout.

Пример: первая версия хранит `owner` в slot 0 и `balance` в slot 1. Если новая версия вставит новую переменную перед owner, она прочитает прежний адрес как другое значение, а balance окажется на неожиданном месте. Код скомпилируется, но состояние будет испорчено.

Новые переменные обычно добавляют в конец совместимой структуры или используют namespaced storage. Автоматическая проверка upgrade safety полезна, но финальный релиз всё равно требует теста миграции на копии реального состояния.

Initializer и upgrade-and-call

Первое развёртывание должно атомарно инициализировать proxy. Если deploy и initialize разнесены, посторонний пользователь может успеть вызвать initialize первым. Аналогичный риск появляется при добавлении новой версии с `reinitializer` без ограничений.

`upgradeToAndCall` позволяет сразу заменить код и вызвать функцию миграции. Это удобно для заполнения новых полей, но объединяет два опасных действия. Неверный calldata способен выдать роль, изменить параметры или вызвать функцию через `delegatecall` в storage proxy.

Проверяйте не только new implementation address, но и data. Нулевой data означает upgrade без дополнительного вызова. Ненулевой data нужно декодировать по ABI новой implementation и сопоставить со storage changes.

Selector clash и прозрачность

ABI использует первые четыре байта hash сигнатуры функции. Разные сигнатуры теоретически могут получить одинаковый selector. Если proxy сам экспонирует пользовательские функции, одна из них способна перехватить вызов, предназначенный implementation.

Transparent pattern появился именно для разделения административных и пользовательских вызовов по caller. Однако расширение TransparentUpgradeableProxy новыми внешними функциями снова создаёт риск конфликтов. OpenZeppelin не рекомендует добавлять такие функции к proxy.

UUPS уменьшает поверхность самого proxy, но требует безопасной implementation. Beacon Proxy также оставляет бизнес-интерфейс логике, а служебное управление переносит в beacon.

Типичные опасные комбинации

  • UUPS implementation за Transparent proxy. Оба паттерна используют один implementation slot. Ошибочная авторизация в UUPS-коде может открыть неожиданный upgrade path не-admin пользователю.
  • Неизвестный ProxyAdmin. Проверен адрес proxy, но owner административного контракта остался нераскрытым.
  • Beacon без инвентаря. Команда обновляет beacon, не зная всех связанных proxy и различий их состояния.
  • Неверифицированная implementation. Пользователь видит знакомый proxy ABI, но не может проверить реальную логику.
  • Пустой initializer. Proxy или implementation остаётся доступной для захвата ролей.
  • Несовместимая layout. Новая версия корректно исполняется, но читает старые slots как другие данные.
  • Мгновенный admin. Один EOA может заменить код без multisig и timelock.
  • Массовый beacon upgrade. Одна ошибка одновременно останавливает целый набор vault или accounts.

Как сравнивать паттерны перед использованием

Transparent удобен, когда важны явный ProxyAdmin и отдельный административный контур. UUPS подходит для множества индивидуально обновляемых proxy и позволяет со временем отказаться от upgradeability, но повышает требования к implementation и `_authorizeUpgrade`. Beacon полезен для однородного парка контрактов, который должен обновляться синхронно.

Ни один паттерн не гарантирует безопасность. Хорошая схема может быть скомпрометирована слабым owner, а сложная – защищена multisig, timelock, проверкой bytecode и публичной процедурой релиза. Сравнивать нужно фактические права и процесс, а не только название библиотеки.

Перед взаимодействием проверьте текущую implementation, историю `Upgraded`, задержку обновления и возможность emergency action. Симуляция транзакции не фиксирует код навсегда: подробнее этот предел разобран в материале о рисках симуляции.

Итог

Transparent, UUPS и Beacon Proxy решают одну задачу разными административными маршрутами. Transparent держит upgrade-interface в proxy и обычно использует ProxyAdmin. UUPS переносит upgrade-код в implementation. Beacon добавляет общий указатель, который меняет логику сразу для множества proxy.

Для пользователя важны четыре вопроса: где находится текущая implementation, кто может её заменить, есть ли задержка и сколько контрактов затронет одно обновление. После этого проверяются storage layout, initializer, calldata миграции и вся цепочка ownership. Только такая onchain-проверка показывает реальный риск обновляемого контракта.