Ссылка на видео-туториал и подробное объяснение GitHub В этом материале речь пойдет про стандарт EIP-2535, также широко известен как Diamond или Multi-Facet Proxy. Стандарт дает возможность создавать модульные, обновляемые смарт контракты, которые обладают рядом преимуществ перед такими стандартами обновляемых контрактов как Transparent и UUPS.   Рассмотрим причины, по которым вы можете использовать стандарт Diamond для своего проекта:   Diamond имеет единый адрес proxy который используется как точка входа для обращения ко всем имплементациям, такой подход упрощает развертывание, тестирование и интеграцию с другими смарт контрактами. Diamond не имеет ограничений по размеру контракта, то есть суммарный размер всех ваших имплементаций может выходить далеко за 24 килобайта. Стандарт позволяет реализовать гибкую систему взаимодействия между гранями(facets), стораджами и библиотеками. С течением времени и по мере необходимости функциональность контракта может быть добавлена, заменена или вовсе удалена. Также по причине описанной в п. 2 контракт не имеет ограничений по количестве функций, которые в нем используются. Контракт Diamond может быть неизменяемым, либо сразу, либо по прошевствию какого-то времени, когда будет принято решение сделать Diamond неизменяемым. Diamond может использовать уже развернутые имплементации, что может положительно сказаться на тратах на газ. Такой подход делает этот стандарт открытым для всех участников сети. Имплементации или грани(facets) независимы друг от друга, но могут совместно использовать внутренние функции, библиотеки и переменные состояния. Добавление/замена/удаление сразу нескольких функций может осуществляться в одной транзакции. Для изменения имплементаций можно использовать DAO и прочих инициализаторов обновления контракта. Diamond вызывает функции своих граней(facet), используя delegatecall, если кому-то нужно вспомнить или узнать как работает delegatecall, милости прошу: solidity-by-example/delegatecall. Когда к Diamond обращается какой-то адрес и вызывает функцию из имплементации, срабатывает fallback. Как работает функция fallback: solidity-by-example/fallback. Внутри функции fallback определяется на какой адрес нужно делегировать вызов на основе первых четырех байтов из msg.data, или же эти 4 байта можно получить сразу обратившись к глобальной переменной msg.sig, таким образом мы получим селектор функции. Подробнее о том что такое селектор функции и как его можно получить: solidity-by-example/function-selector. Благодаря свойствам fallback и delegatecall, Diamond может выполнять функцию грани(facet), как если бы она была определена в самом Diamond. При вызове функции из имплементации(читай грани) благодаря delegatecall значения msg.sender и msg.value остаются неизменными, а также считывается и записывается только хранилище diamond, то есть, можно сказать, что грань(facet), к которой мы обращаемся дает только инструкцию, в которой сказано как обращаться с хранилищем diamond. Рассмотрим концептуальную реализацию функции fallback() в основном контракте diamond: Подведем итоги по строению Diamond:   В контракте есть только одна функция fallback. В зависимости от селектора вызов делегируется на нужную имплементацию. Каждому селектору соответствует свой адрес грани. Все состояния хранятся на контракте Diamond, на гранях(facets) только логика. Для облуживания стораджа контракта Diamond используются библиотеки. Количество стораджей неограниченно, они могут появляться по мере необходимости и появления нового функционала и переменных. Чтобы значения в сторадже не перепутались и не перезаписывались хаотичным образом, каждый сторадж организуется в структуру, в свою очередь структура, помещается в определенную ячейку, которая является точкой входа, для обращения к конкретной переменной структуры. Рассмотрим следующий пример: мы имеем две грани, которые обращаются к одному и тому же стораджу используя библиотеку. Эти грани будут представлять из себя частичную реализацию стандарта ERC-721. Как правильно обновлять сторадж: Чтобы добавить новые переменные состояния в структуру хранилища, добавьте их в самый конец структуры. Если вам нужно добавить mapping, помещайте его также в конец структуры. Имена переменных состояния могут быть изменены, но это может привести к путанице, если разные грани используют разные имена для одних и тех же мест хранения. Поэтому для всех граней рекомендуется использовать библиотеки где сторадж определен одинаково, как с точки зрения нейминга, так и с точки зрения порядка следования переменных в структуре. Что не следует делать при изменении стораджа: Не добавляйте новые переменные состояния в начало или середину структур. Выполнение этого приводит к тому, что новая переменная состояния перезаписывает существующие данные переменной состояния и все переменные состояния после неё, так как новая переменная состояния ссылается на неправильное место хранения. Допустим у вас в структуре стораджа есть mapping, который возвращает другую структуру, такая практика допустима, если возвращаемая структура не будет обновляться на протяжении всего времени существования Diamond. Если же в будущем появится потребность изменить возвращаемую мапингом структуру, то вам придется создавать новый мапинг для этой структуры. Не добавляйте новые переменные состояния в структуры, которые используются в массивах. При использовании хранилища Diamond не используйте один и тот же номер слота(ERC721_POSITION в примере) для разных хранилищ. Это очевидно. Два разных хранилища в одном и том же месте будут перезаписывать друг друга. Не допускайте, чтобы какая-либо грань могла вызывать selfdestruct. Просто не разрешайте команде selfdestruct существовать в любом исходном коде грани и не разрешайте вызывать эту команду через вызов delegatecall. Потому что selfdestruct может удалить грань, которая используется алмазом, или selfdestruct может быть использован для удаления основного прокси-контракта Diamond. Грани могут совместно использовать одни и те же стораджи. Грани могут использовать одни и те же внутренние функции и библиотеки. Таким образом, грани являются независимыми друг от друга единицами, но при этом они могут обращаться к одинаковым библиотекам для изменения состояния хранилища.   На диаграмме выше: Только FacetA может получить доступ к DataA Только FacetB может получить доступ к DataB Только код Diamond может получить доступ к DataD Доступ к DataAB имеют как FacetA так и FacetB К DataABD можно обратиться откуда угодно Любой Diamond должен реализовывать интерфейс IDiamond.   В свою очередь интерфейс IDiamond наследуется другим интерфейсом IDiamondCut, который содержит в себе одну функцию diamondCut. Функция diamondCut обновляет любое количество функций из любого количества фасетов за одну транзакцию. Выполнение всех изменений в рамках одной транзакции предотвращает повреждение данных, которое может произойти при обновлениях, выполняемых в рамках нескольких транзакций. После добавления/замены/удаления функций аргумент _calldata выполняется на адрес _init через delegatecall. Это выполнение выполняется для инициализации данных или настройки или удаления чего-либо необходимого или больше не нужного после добавления, замены и/или удаления функций. Можно провести аналогию с функцие конструктором, которая также инициализирует какие-то начальные значения. Если значение _init равно нулевому адресу address(0) то выполнение _calldata пропускается. В этом случае _calldata может содержать 0 байт или пользовательскую информацию, которая отправится на etherscan через событие DiamondCut. Diamond должен поддерживать проверку аспектов и функций путем реализации интерфейса IDiamondLoupe. Таким образом с помощью интерфейса IDiamondCut мы обновляем Diamond, а с помощью IDiamondLoupe можем отслеживать грани и их состояние на наличие тех или иных селекторов. Напишем обычный erc20 весь функционал которого будет разбит на 4 грани. Это будет сделано для наглядности, ведь внутренний механизм работы стандарта erc20 известен всем или многим. Но перед этим нам нужно разобраться с обслуживающими гранями, которые отвечают за изменение функциональности и мониторинг уже имеющихся граней и функциональности. Эти целям будет служить библиотека LibDiamond и три грани: DiamondCutFacet - для изменения функциональности, DiamondLoupeFacet - для мониторинга граней и функциональности контракта Diamond, OwnershipFacet - грань, которая помогает администрировать доступ к функции diamindCut, чтобы имплементации не менялись кем попало. Рассмотрим код библиотеки LibDiamond, она будет дана в сокращенном виде, полный код будет в открытом доступе на github: Рассмотрим грань DiamondCutFacet, отвечающая за изменение функционала Diamond: Рассмотрим грань DiamondLoupeFacet, код будет показан в сокращенном виде, полный код можно посмотреть на github: Последняя обязательная грань OwnershipFacet:   Наконец рассмотрим сам контракт Diamond, а затем поймем как правильно и в каком порядке нужно деплоить Diamond и грани. Контракт Diamond: Приступим к тестированию, полный код тестирования можно посмотреть также на github. Мы развернули базовую часть Diamond, которая необходима для управления имплементациями. Теперь реализуем на Diamond функционал токена ERC20, со всеми функциями, которые присущи этому стандарту. Все хранилище токена будет разбито на три стораджа, к которым будут обращаться 4 грани, рассмотрим архитектуру граней и стораджей на диаграмме: Библиотека LibConstants будет отвечать за хранение таких констант токена, как name, symbol, decimals и адрес админа, которому будут доступны функции mint(), burn() из грани SupplyRegulatorFacet. Следующая библиотека LibBalances отвечает за хранение балансов и сопутствующих функций: Для инициализации констант из библиотеки LibConstants нужен отдельный контракт с функцией инициализации, которая будет вызвана сразу после добавления грани в Diamond. Наконец напишем контракт грани, которая будет возвращать некоторые константы, обращаясь к библиотеке LibConstants: Продолжим тестирование, в ходе которого мы проинициализируем переменные хранилища, с помощью функции initERC20 из контракта DiamondInit, а также добавим новую грань ConstantsFacet: Напишем ещё одну грань для нашего контракта, благодаря которой мы сможем вызывать функцию transfer и передавать наши токены другим адресам. Продолжим тестирование в ходе которого в контракт будет добавлена грань BalancesFacet: Наконец напишем грань, после добавления которой у нас будет полноценный erc20, а именно возможность апрувить токены и переводить их с другого адреса используя transferFrom, помимо самой грани нам ещё нужна библиотека для обслуживания хранилища, в котором будет записано, кто сколько кому заапрувил.  Контракт грани: Вернемся к тестированию, добавим грань AllowancesFacet и проверим корректно ли она работает, порядок деплоя и добавления ничем не отличается от предыдущих граней: Напишем ещё одну грань, которая будет регулировать эмиссию токена, в ней будет две функции: mint и burn, их сможет вызывать только админ, который был проинициализирован в функции initERC20. Продолжим тестирование: На этом тестирование функционала ERC20 закончено и мы можем отметить что работает оно корректно. Но, что если мы хоти сделать Diamond не обновляемым, например, в процессе разработке мы пришли к какой-то стабильной версии контракт и нас больше нет необходимости вызывать функцию diamondCut? Для это мы просто удалим селектор функции diamondCut:   После выполнения этого действия контракт станет не обновляемым и статичным, так что будьте осторожны, если решитесь удалить функцию diamondCut из Diamond. Ссылка на видео-туториал и подробное объяснение: https://www.youtube.com/watch?v=42TUqDW74v8 GitHub: https://github.com/davydovMikhail/multi-proxy-contract Остались вопросы? С чем-то не согласны? Пишите комментарии Поддержать автора криптовалютой: 0x021Db128ceab47C66419990ad95b3b180dF3f91F  Developer