Прокси-контракты — важный инструмент для разработчиков смарт-контрактов. Сегодня в контрактной системе существует множество агентских моделей и соответствующих правил использования. Ранее мы изложили лучшие практики безопасности для обновляемых прокси-контрактов.
В этой статье мы представим еще одну модель прокси, популярную в сообществе разработчиков, — модель алмазного прокси.
Контракт алмазного агентства, также известный как «Бриллиант», представляет собой шаблон проектирования для смарт-контрактов Ethereum, представленный Предложением по улучшению Ethereum (EIP) 2535.
Модель Diamond позволяет контракту иметь неограниченную функциональность за счет разделения его функциональности на более мелкие контракты (также известные как «ломтики»). Алмазы действуют как прокси, направляя вызовы функций к соответствующим аспектам.
Конструкция алмазного режима может решить проблему максимального размера контракта в сети Ethereum. Разбивая большой контракт на более мелкие элементы, ромбовидная структура позволяет разработчикам создавать более сложные и многофункциональные смарт-контракты, не ограничиваясь ограничениями по размеру.
По сравнению с традиционными контрактами с возможностью обновления Diamond Proxy обеспечивает колоссальную гибкость. Они позволяют частично обновлять контракты, добавляя, заменяя или удаляя выбранные части функциональности, не затрагивая другие части.
В этой статье представлен обзор EIP-2535, включая сравнение с широко используемым режимом прозрачного прокси и режимом прокси UUPS, а также соображения по его безопасности для сообщества разработчиков.
В контексте EIP-2535 «ромб» — это прокси-контракт, реализация функциональности которого обеспечивается различными логическими контрактами, называемыми «гранями».
Представьте себе, что настоящий алмаз имеет разные стороны, называемые гранями, и соответствующий контракт Ethereum Diamond также имеет разные грани. Каждый контракт, заимствующий функциональность у алмаза, представляет собой отдельную сторону или грань.
Алмазный стандарт использует аналогию для расширения функциональности «бриллиантовой огранки» с целью добавления, замены или удаления граней и особенностей.
Кроме того, Diamond Standard предоставляет функцию под названием «Бриллиантовая лупа», которая возвращает информацию о гранях и функциональных возможностях, присутствующих в алмазе.
По сравнению с традиционной моделью агентства, «ромб» эквивалентен агентскому договору, в то время как различные «грани» соответствуют договору на реализацию. Различные грани алмазного прокси могут совместно использовать внутренние функции, библиотеки и переменные состояния. Ключевые компоненты алмаза следующие:
Центральный контракт, который действует как прокси-сервер, маршрутизируя вызовы функций к соответствующему аспекту. Он содержит сопоставление селекторов функций с адресами «фасетов».
Единый контракт, реализующий определенную функциональность. Каждая грань содержит набор функций, которые может вызывать ромб.
представляет собой набор стандартных функций, определенных в EIP-2535, которые предоставляют информацию о гранях и селекторах функций, используемых в алмазах. Алмазные лупы позволяют разработчикам и пользователям изучать и понимать структуру алмаза.
Функции для добавления, замены или удаления граней в алмазе и соответствующие им селекторы функций. Огранку алмазов могут выполнять только уполномоченные лица (например, владелец алмаза или многоподписной контракт).
Подобно традиционным прокси-серверам, при вызове функции на Diamond Proxy будет активирована резервная функция прокси-сервера. Главное отличие от Diamond Proxy заключается в том, что в резервной функции есть сопоставление selectorToFacet, которое хранит и определяет, какой логический адрес контракта имеет реализацию вызываемой функции. Затем он использует delegatecall для выполнения функции, как и традиционный делегат.
Все прокси используют функцию fallback() для делегирования вызовов функций внешнему адресу. Ниже представлена реализация алмазного прокси и традиционного прокси.
Стоит отметить, что их блоки ассемблерного кода очень похожи, поэтому единственное различие — это адрес фасета в вызове прокси-делегата diamond и адрес impl в вызове традиционного прокси-делегата.
Основное отличие заключается в том, что в алмазных прокси-серверах адрес фасета определяется хэш-картой из msg.sig вызывающей стороны (селектор функций) в адрес фасета, тогда как в традиционных прокси-серверах адрес impl не зависит от ввода вызывающей стороны.
Функция резервного копирования Diamond Proxy
Традиционная функция резервного прокси-сервера
Сопоставление SelectorToFacet определяет, какой контракт содержит реализацию каждого селектора функций. Сотрудникам проекта часто приходится добавлять, заменять или удалять это сопоставление селекторов функций с контрактами на реализацию. В EIP-2535 указано: Для достижения этого должна быть функция diamondCut(). Ниже приведен пример интерфейса.
Каждая структура FacetCut содержит адрес фасета и четырехбайтовый массив селекторов функций, которые необходимо обновить в контракте Diamond Proxy. FaceCutAction позволяет пользователям добавлять, заменять и удалять селекторы функций. Реализация функции diamondCut() должна включать адекватный контроль доступа, предотвращать конфликты слотов хранения и выполнять восстановление в случае сбоя.
Для того чтобы проверить, какие функции выполняет алмазный агент и какие грани он использует, мы использовали «алмазную лупу». «Бриллиантовая лупа» — это специальный аспект, реализующий следующий интерфейс, определенный в EIP-2535:
Функция facets() должна возвращать адреса всех фасетов и их четырехбайтовые селекторы функций. Функция facetFunctionSelectors() должна возвращать все селекторы функций, поддерживаемые определенным фасетом. Функция facetAddresses() должна возвращать адреса всех граней, используемых алмазом.
Функция facetAddress() должна возвращать фасет, поддерживающий заданный селектор, или адрес (0), если он не найден. Обратите внимание, что не должно быть более одного адреса аспекта с одним и тем же селектором функций.
Учитывая, что Diamond Proxy делегирует различные вызовы функций разным контрактам реализации, крайне важно правильно управлять слотами хранения, чтобы предотвратить конфликты. В EIP-2535 упоминается несколько методов управления слотами хранения.
Этот аспект может объявлять переменные состояния в структуре. Этот аспект может использовать любое количество структур, каждая из которых имеет свое место хранения. Каждая конструкция имеет определенное место в хранилище контракта. Аспекты могут объявлять собственные переменные состояния, но они не могут конфликтовать с местами хранения переменных состояния, объявленных другими аспектами. EIP-2535 предоставляет образец библиотеки и контракта на хранение алмазов, как показано ниже:
App Storage — это более специализированная версия Diamond Storage. Этот шаблон используется для того, чтобы сделать совместное использование переменных состояния аспекта более удобным и простым. Структура хранилища приложения определена таким образом, чтобы содержать любое количество и тип переменных состояния, необходимых приложению. Аспект всегда объявляет структуру AppStorage как первую и единственную переменную состояния, расположенную в слоте хранилища 0. Различные аспекты затем могут получать доступ к переменным из этой структуры.
Существуют и другие стратегии управления слотами хранения, включая гибрид Diamond Storage и AppStorage. Например, некоторые структуры являются общими для разных аспектов, в то время как другие уникальны для определенного аспекта. Во всех случаях очень важно предотвратить случайные столкновения резервуаров.
Сравнение с прозрачным прокси и UUPS прокси
В настоящее время сообщество разработчиков Web3 использует два основных режима прокси: режим прозрачного прокси и режим прокси UUPS. В этом разделе мы кратко сравним режим Diamond Proxy с режимами Transparent Proxy и UUPS Proxy.
1.EPI-2535:https://eips.ethereum.org/EIPS/eip-2535 #Фасеты,% 20 Состояние% 20 Переменные% 20 и% 20 Алмаз% 20 Хранилище
2.EPI-1967:https://eips.ethereum.org/EIPS/eip-1967
3. Реализация эталонного прокси-сервера Diamond: https://github.com/mudgen/Diamond
4.Реализация OpenZeppelin: https://github.com/OpenZeppelin/openzeppelin-contracts/tree/v4.7.0/contracts/proxy
Агенты и обновляемые решения представляют собой более сложные системы. OpenZeppelin предоставляет библиотеки кода и полную документацию для обновляемых агентов UUPS, Transparent и Beacon. Однако, несмотря на то, что OpenZeppelin подтверждает преимущества модели Diamond Proxy, они все же решили не включать реализацию Diamond EIP-2535 в свою библиотеку.
Поэтому разработчикам, использующим существующие сторонние библиотеки или реализующим это решение самостоятельно, следует проявлять особую осторожность при его реализации. Здесь мы составили список лучших практик обеспечения безопасности, к которым может обратиться сообщество разработчиков.
Разбивая логику контракта на более мелкие и более управляемые модули, разработчикам становится проще тестировать и проверять свой код.
Кроме того, такой подход позволяет разработчикам сосредоточиться на конкретных аспектах создания и поддержки контрактов, а не на управлении сложной монолитной кодовой базой. Конечным результатом является более гибкая и модульная кодовая база, которую можно легко обновлять и изменять, не затрагивая другие части контракта.
Источник: Aavegotchi Github
При развертывании контракта DiamondProxy необходимо добавить адрес контракта DiamondCutFacet к контракту DiamondProxy и реализовать функцию diamondCut(). Функция diamondCut() используется для добавления, удаления или замены граней и функций. Без DiamondCutFacet и diamondCut() Diamond Agent не может работать должным образом.
Источник: Diamond-3-Hardhat от Mugen
При добавлении новой переменной состояния в структуру хранения в смарт-контракте ее необходимо добавить в конец структуры. Добавление новой переменной состояния в начало или в середину структуры приводит к тому, что новая переменная состояния перезаписывает существующие данные переменной состояния, а любые переменные состояния после новой переменной состояния могут ссылаться на неправильную область памяти.
Шаблон AppStorage требует, чтобы для прокси-сервера Diamond была объявлена одна и только одна структура, и чтобы эта структура была общей для всех аспектов. Если требуется несколько структур, следует использовать шаблон DiamondStorage.
Не помещайте структуру непосредственно внутрь другой структуры, если вы не уверены, что не собираетесь добавлять больше переменных состояния во внутреннюю структуру. Невозможно добавить новые переменные состояния во внутреннюю структуру при обновлении без перезаписи слотов хранения переменных, объявленных позже в структуре.
Решение состоит в том, чтобы добавить новые переменные состояния в отображенную в памяти структуру вместо того, чтобы помещать структуру непосредственно внутрь структуры. Слоты хранения переменных в отображении рассчитываются по-разному и не являются смежными в хранилище.
Размер массива будет зависеть от размера структуры. Когда к структуре добавляется новая переменная состояния, она изменяет размер и компоновку этой структуры.
Это может вызвать проблемы, если структура используется как элемент массива. Если размер и компоновка структуры изменятся, то размер и компоновка массива также изменятся, что может вызвать проблемы с индексацией или другими операциями, которые зависят от согласованного размера и компоновки структуры.
Подобно другим шаблонам прокси, каждая переменная должна иметь уникальный слот хранения. В противном случае две разные структуры в одном и том же месте будут перезаписывать друг друга.
Функция initialize() обычно используется для установки важных переменных, таких как адрес привилегированной роли. Если эта функция не инициализирована при развертывании контракта, злоумышленники могут вызвать контракт и управлять им.
Рекомендуется добавить соответствующий контроль доступа к функции инициализации/настройки или гарантировать, что функция вызывается при развертывании контракта и не может быть вызвана повторно.
Если бы какой-либо аспект контракта мог вызвать функцию selfdestruct(), это потенциально могло бы повредить весь контракт, что привело бы к потере средств или данных. Это чрезвычайно опасно в схеме прокси-сервера Diamond, поскольку к хранилищу и данным прокси-контракта могут получить доступ несколько сторон.
В настоящее время мы видим все больше проектов, использующих модель прокси-сервера Diamond в своих смарт-контрактах. Он обеспечивает гибкость и другие преимущества по сравнению с традиционными прокси-серверами.
Однако дополнительная гибкость может также означать более широкую поверхность для атак для злоумышленников. Мы надеемся, что эта статья поможет сообществу разработчиков понять механизм работы модели Diamond Proxy и ее аспекты безопасности.
В то же время команда проекта должна проводить тщательное тестирование и сторонние аудиты, чтобы снизить риск уязвимостей, связанных с реализацией контракта на поставку алмазов по доверенности.
CertiK продолжит публиковать подобные технические статьи, чтобы помочь большему количеству разработчиков вести безопасную разработку. Подпишитесь на нас, чтобы получать больше подобной информации и новостей!

