February 22, 2023

Как внедрить стандарт Diamond

Diamond Standard, EIP-2535, был создан Ником Маджем как стандартизированная архитектура для реализации смарт-контрактов через прокси. В этой статье мы рассмотрим плюсы использования Diamond Standard и принципы его работы.

Общий обзор

Алмазный стандарт работает путем развертывания контракта под названием Diamond.sol. Diamond.sol, затем вызывает другие смарт-контракты через delegatecall(). Это позволяет Diamond.solвыполнять код вызываемого контракта в контексте Diamond.sol. Все контракты, которые вызываются с помощью, delegatecall()называются фасетами. Фасеты можно заменять, удалять и добавлять с течением времени, что позволяет разработчикам создавать модульные приложения на блокчейнах, совместимых с EVM. Алмазный стандарт требует от вас реализации DiamondLoupeFacet.sol. DiamondLoupeFacet.solотвечает за документирование других аспектов вашего протокола. DiamondLoupeFacet.solобеспечивает прозрачность, позволяя пользователям видеть адреса и функции фасетов. Еще одно требование Алмазного стандарта DiamondCutFacet.sol. DiamondCutFacet.solотвечает за все обновления вашего приложения.DiamondCutFacet.solтакже генерирует события при вызове, обеспечивая другой уровень прозрачности для пользователей протокола. Хотя это и не является обязательным требованием, LibDiamond.solэто библиотека, которая предоставляет множество вспомогательных функций для написания приложения с помощью стандарта Diamond. При создании приложения с помощью Diamond Standard вы можете использовать один из двух способов сохранения переменных состояния. Их называют App Storageи Diamond Storage. Мы рассмотрим все компоненты Diamond Standard более подробно в оставшейся части статьи.

Алмазный стандарт имеет три различных варианта реализации. К счастью, если вы решите, что выбрали неправильную реализацию, можно обновить приложение до другой реализации. diamond-1является самой базовой реализацией. Сложность diamond-1проще всего понять, а затраты на газ самые низкие. Не рекомендуется вызывать DiamondLoupeFacet.solфункции по цепочке с diamond-1(или diamond-2) из-за высоких затрат на газ. diamond-3, с другой стороны, выбирает оптимизацию DiamondLoupeFacet.solфункций вызова в цепочке. Компромисс заключается в том, что звонки обходятся дороже DiamondCutFacet.sol. diamond-2очень похоже наdiamond-1, но оптимизирует затраты на газ по сравнению со сложностью для понимания. Вам, как разработчику, решать, какая из этих реализаций лучше всего подходит для вашего приложения.

Из-за сложности Diamond Standard рекомендуется следовать общему шаблону при создании приложений. Я свяжу эти шаблоны внизу статьи вместе с дополнительными материалами для чтения.

Зачем использовать Алмазный стандарт?

Основная критика стандарта Diamond — его сложность (надеюсь, я смогу помочь решить эту проблему). Несмотря на сложность, Diamond Standard обеспечивает следующие преимущества:

  • Практических ограничений на размер вашего приложения нет. Все смарт-контракты имеют максимальный размер 24 КБ. Поскольку Diamond Standard использует Diamond.solсвои delegatecall()грани, размер контракта остается относительно небольшим.
  • Вы используете только один адрес для множества функций. Опять же, благодаря delegatecall(), вы можете добавить все необходимые функции в один смарт-контракт. Это включает в себя реализацию токенов с тем же адресом, что и ваш протокол.
  • Он обеспечивает организованный способ обновления ваших смарт-контрактов. Это может помочь аудиторам понять и защитить ваше приложение.
  • Это позволяет вам постепенно обновлять свои смарт-контракты. Вместо повторного развертывания всего контракта вы можете просто добавить, заменить или удалить фасет, чтобы обеспечить желаемую функциональность вашего приложения.
  • Это обеспечивает высокий уровень прозрачности для вашего прокси-контракта. Как упоминалось ранее, DiamondLoupeFacet.solпозволяет пользователям видеть, где находятся ваши функции в блокчейне и что они выполняют. Посетите https://louper.dev/ , чтобы проанализировать функциональность смарт-контрактов, использующих преимущества Diamond Standard.
  • Практически нет ограничений на количество аспектов, которые можно использовать в вашем приложении. Единственный способ, которым вы не сможете добавить больше фасетов в свой протокол, — это Diamond.solнехватка места для хранения данных фасетов. Для этого потребуется смехотворно большое количество граней, вплоть до того, что это практически невозможно.
  • Фасеты могут быть повторно использованы несколькими Diamond.solконтрактами. Это позволяет экономить на газе при развертывании приложений, которые заимствуют функциональные возможности.
  • При желании вы можете запустить оптимизатор с более высокими настройками. Оптимизатор Solidity помогает оптимизировать затраты газа при вызове внешних функций. Однако это увеличивает байт-код развертывания контракта. Это может привести к тому, что ваш смарт-контракт превысит максимальный размер смарт-контракта. Поскольку вы можете развертывать столько аспектов, сколько хотите, вы можете настроить оптимизатор на столько запусков, сколько пожелаете, не беспокоясь о том, что ваш контракт слишком велик.

Есть много причин, по которым вы хотели бы использовать Diamond Standard в своем приложении, но не забывайте слишком усложнять свое приложение. Если ваш протокол может работать с одним смарт-контрактом и ни одно из вышеперечисленных преимуществ не применимо к вашему приложению, нет необходимости использовать Diamond Standard. В конечном итоге это только усложнит ваше приложение и увеличит расходы на газ. Однако, если вы находитесь в ситуации, когда вам нужен шаблон прокси для вашего приложения, я лично рекомендую использовать Diamond Standard.

Хранилище приложений и алмазное хранилище

Архитектура переменных состояния — один из наиболее важных аспектов вашего блокчейн-приложения. Я решил сначала рассмотреть этот раздел, чтобы дать вам хорошее представление о том, как переменные состояния управляются с помощью Diamond Standard. Очевидно, что прокси в значительной степени полагаются на delegatecall()выполнение кода из ваших фасетных контрактов в контексте вашего основного контракта ( Diamond.sol). Поскольку все хранилище наших переменных состояния хранится в Diamond.sol, нам нужно убедиться, что наши переменные не перезаписывают друг друга. Прежде чем мы рассмотрим правильный способ организации наших переменных состояния, давайте рассмотрим пример неправильного способа управления переменными состояния.

pragma solidity^0.8.17;0.8.17;

contract Main {
uint256 public verySpecialVar;
uint256 public notSoSpecialVar;
// delegate calls SpecialVarManager to update verySpecialVar
function setVerySpecialVar(address _specialVarManager) external {
_specialVarManager.delegatecall(
abi.encodeWithSignature("writeSpecialVar()")
);
}
// delegate calls NotSpecialVarManager to update notSoSpecialVar
function setNotSoSpecialVar(address _notSpecialVarManager) external {
_notSpecialVarManager.delegatecall(
abi.encodeWithSignature("writeNotSpecialVar()")
);
}

}
contract SpecialVarManager {
uint256 verySpecialVar;
function writeSpecialVar() external {
verySpecialVar = 100;
}
}
contract NotSpecialVarManager {
uint256 notSoSpecialVar;
function writeNotSpecialVar() external {
notSoSpecialVar = 50;
}
}

Если вы позвоните, setVerySpecialVar()вы увидите, verySpecialVarчто был обновлен до 100! Теперь давайте позвоним setNotSoSpecialVarи посмотрим, что произойдет. Мы видим, notSoSpecialVarчто он все еще не инициализирован и равен 0. Если мы проверим, то verySpecialVarувидим, что сейчас он установлен на 50. Почему? Ну, в схеме хранения NotSpecialVarManagerнет verySpecialVar. Поэтому, когда мы вызываем writeNotSpecialVar()via, delegatecall()мы сообщаем Mainоб обновлении слота хранения с 0 до 50. Solidity не заботится о том, как вы называете свои переменные; он смотрит только на положение слота для хранения.

Имея это в виду, нам нужен способ организовать наши переменные состояния, чтобы мы не перезаписывали наши слоты хранения. Первый способ сделать это — с помощью Diamond Storage.

Diamond Storage использует количество слотов для хранения Solidity в смарт-контракте (2²⁵⁶). Теория, лежащая в основе Diamond Storage, заключается в том, что, поскольку слотов для хранения так много, что если мы хэшируем уникальное значение, мы получим случайный слот для хранения, который почти наверняка не будет конфликтовать с другим слотом для хранения. Это может показаться рискованным, но на самом деле это тот же процесс, который Solidity использует для хранения отображений и динамических массивов. Diamond Storage дает возможность вашим фасетам сохранять переменные состояния, специфичные для их контракта, а также позволяет фасетам совместно использовать переменные состояния, если это необходимо.

Поскольку сложность Diamond Standard требует небольшой настройки, для этих примеров хранения я не буду использовать Diamond Standard. Основная цель этого раздела — понять принципы хранения переменных состояния. Реализация в Diamond Standard в основном такая же.

pragma solidity^0.8.17;0.8.17;

library SharedLib {
// struct that with state variable
struct DiamondStorage {
uint256 sharedVar;
}
// returns a storage variable with our state variable
function diamondStorage() internal pure returns(DiamondStorage storage ds) {
// gets a "random" storage positon by hashing a string
bytes32 storagePosition = keccak256(abi.encode("Diamond.Storage.SharedLib"));
// assigns our struct storage slot to storage position
assembly {
ds.slot := storagePosition
}

}
}
// not the actual Diamond Standard
contract PseudoDiamond {
// delegate calls Facet1 to update sharedVar
function writeToSharedVar(address _facet1, uint256 _value) external {
// writes via delegate call
_facet1.delegatecall(
abi.encodeWithSignature("writeShared(uint256)", _value)
);
}
// delegate calls Facet2 to read sharedVar
function readSharedVar(address _facet2) external returns (uint256) {
// returns result of delegate call
(bool success, bytes memory _valueBytes) = _facet2.delegatecall(
abi.encodeWithSignature("readShared()")
);
// since return value is bytes array we use assembly to retrieve our uint
bytes32 _value;
assembly {
let location := _valueBytes
_value := mload( add(location, 0x20) )
}
return uint256(_value);
}
}
contract Facet1 {
function writeShared(uint256 _value) external {
// initializes the storage struct by calling library function
SharedLib.DiamondStorage storage ds = SharedLib.diamondStorage();
// writes to shared variable
ds.sharedVar = _value;

}
}
contract Facet2 {

function readShared() external view returns (uint256) {
// initializes the storage struct by calling library function
SharedLib.DiamondStorage storage ds = SharedLib.diamondStorage();

// returns shared variable
return ds.sharedVar;

}
}

Если вы позвоните PseudoDiamond.writeSharedVar(), то PseudoDiamond.readSharedVar()увидите свое значение. Используя библиотеку и индексируя «случайный» слот для хранения, мы можем обмениваться переменными между двумя смарт-контрактами. Когда мы используем delegatecall()оба аспекта, он смотрит на позицию хранения структуры DiamondStorageдля доступа к этой переменной. Благодаря явной связи между аспектами того, где мы хотим хранить наши данные, мы предотвращаем коллизии переменных состояния. Если вы хотите иметь переменные состояния только для одного из ваших аспектов, вы можете просто создать библиотеку, аналогичную SharedLib, и реализовать ее только в этом конкретном аспекте.

Хранилище приложений работает немного по-другому. Вы создаете файл Solidity, а внутри этого файла создаете структуру с именем AppStorage. Затем вы помещаете в эту структуру столько переменных состояния, сколько хотите, включая другие структуры. Затем первое, что вы делаете внутри своих смарт-контрактов, — это инициализируете их AppStorage. Это устанавливает слот хранения 0 в начало структуры. Это создает удобочитаемое общее состояние между контрактами. Давайте посмотрим на пример!

pragma solidity^0.8.17;

// struct with state variable
struct StateVars {
uint256 sharedVar;
}
// our app storage struct
struct AppStorage {
StateVars state;
}
// not the actual Diamond Standard
contract PseudoDiamond {
AppStorage s;
// delegate calls Facet1 to update sharedVar
function writeToSharedVar(address _facet1, uint256 _value) external {
// writes via delegate call
_facet1.delegatecall(
abi.encodeWithSignature("writeShared(uint256)", _value)
);
}
// delegate calls Facet2 to read sharedVar
function readSharedVar(address _facet2) external returns (uint256) {
// returns result of delegate call
(bool success, bytes memory _valueBytes) = _facet2.delegatecall(
abi.encodeWithSignature("readShared()")
);
// since return value is bytes array we use assembly to retrieve our uint
bytes32 _value;
assembly {
let location := _valueBytes
_value := mload( add(location, 0x20) )
}
return uint256(_value);
}
}
contract Facet1 {
AppStorage s;
function writeShared(uint256 _value) external {
// writes to our state variable in storage
s.state.sharedVar = _value;
}
}
contract Facet2 {
AppStorage s;

function readShared() external view returns (uint256) {
// returns state variable from app storage
return s.state.sharedVar;
}
}

Если вы посмотрите на значение в слоте хранения 0 для PseudoDiamond, вы увидите 100, что является нашим значением! Важное замечание о App Storage: если вам нужно обновить AppStorage после развертывания, обязательно добавьте новые переменные состояния в конец AppStorage, чтобы предотвратить коллизии хранилища. Лично я предпочитаю App Storage для организации Diamond Storage, но оба они выполняют свою работу. Также стоит отметить, что Diamond Storage и App Storage не являются эксклюзивными. Даже если вы используете App Storage для управления переменными состояния LibDiamond.sol, используйте Diamond Storage для управления данными аспектов.

Diamond.sol

Как упоминалось ранее, Diamond.solэто смарт-контракт, который вызывается при взаимодействии с вашим приложением. Если это помогает думать об этом таким образом, Diamond.sol«управляет» остальной частью вашего приложения. Все фасетные функции будут казаться Diamond.solсобственными функциями .

Давайте сначала посмотрим, что происходит в конструкторе diamond-1.

// This is used in diamond constructor
// more arguments are added to this struct
// this avoids stack too deep errors
struct DiamondArgs {
address owner;
address init;
bytes initCalldata;
}

contract Diamond {
constructor(IDiamondCut.FacetCut[] memory _diamondCut, DiamondArgs memory _args) payable {
LibDiamond.setContractOwner(_args.owner);
LibDiamond.diamondCut(_diamondCut, _args.init, _args.initCalldata);
// Code can be added here to perform actions and set state variables.
}
// rest of code
}

Во-первых, вне контракта у нас есть структура, которая форматирует наши данные. Затем мы передаем эту структуру в качестве параметра вместе с другой структурой, которая выглядит так:

struct FacetCut {
address facetAddress;address facetAddress;
FacetCutAction action;
bytes4[] functionSelectors;
}

FacetCutпредоставляет адрес фасета, действие, которое мы хотим выполнить с фасетом (добавить, заменить или удалить), и селекторы функций для функций фасета. Затем мы устанавливаем владельца контракта и добавляем любые данные фасета, предоставленные Diamond. Позже мы более подробно рассмотрим, как это работает. После этого, если мы захотим, мы можем инициализировать переменные состояния. Имейте в виду, что вы никогда не должны выполнять какие-либо назначения переменных состояния в конструкторе фасета, потому что он будет выполнять эту операцию внутри смарт-контракта фасета, а не Diamond.sol.

Пока все выглядит довольно просто, поэтому давайте посмотрим, что происходит в конструкторе для diamond-2и diamond-3.

contract Diamond {

constructor(address _contractOwner, address _diamondCutFacet) payable {
LibDiamond.setContractOwner(_contractOwner);
// Add the diamondCut external function from the diamondCutFacet
IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1);
bytes4[] memory functionSelectors = new bytes4[](1);
functionSelectors[0] = IDiamondCut.diamondCut.selector;
cut[0] = IDiamondCut.FacetCut({
facetAddress: _diamondCutFacet,
action: IDiamondCut.FacetCutAction.Add,
functionSelectors: functionSelectors
});
LibDiamond.diamondCut(cut, address(0), "");
}
// rest of code
}

На этот раз мы передаем только владельца контракта и адрес DiamondCutFacet.sol. Далее мы назначаем владельца Алмаза. После этого мы добавляем diamondCut()функцию, чтобы мы могли добавить больше аспектов. Мы делаем это, сначала инициализируя массив памяти одним элементом типа struct FacetCut, который мы видели ранее. Затем мы инициализируем другой массив одним элементом. На этот раз это массив bytes4, и он используется для хранения diamondCut()селектора функций . Затем мы назначаем селектор функции массиву, вызывая функцию интерфейса Diamond Cut diamondCutи получая ее селектор. После этого мы можем назначить cut[0]. Мы используем адрес DiamondCutFacet.sol, add (потому что мы хотим добавить эту грань к нашему Алмазу) и массив селекторов функций. Наконец, мы фактически добавляемdiamondCut. Это много незнания того, что происходит под капотом, поэтому, если это поможет, вы можете перечитать этот раздел после того, как мы пройдемся по DiamondCut.sol. Пока просто поймите, что мы добавляем фасеты в конструкторе.

Последняя часть — Diamond.solэто fallback()функция. Вот как мы будем называть наши грани в Diamond Standard. Он выглядит почти одинаково во всех трех реализациях Diamond Standard, так что давайте рассмотрим его!

// Find facet for function that is called and execute the
// function if a facet is found and return any value.
fallback() external payable {
LibDiamond.DiamondStorage storage ds;
bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION;
// get diamond storage
assembly {
ds.slot := position
}
// get facet from function selector
address facet = address(bytes20(ds.facets[msg.sig]));
require(facet != address(0), "Diamond: Function does not exist");
// Execute external function from facet using delegatecall and return any value.
assembly {
// copy function selector and any arguments
calldatacopy(0, 0, calldatasize())
// execute function call using the facet
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
// get any return value
returndatacopy(0, 0, returndatasize())
// return any return value or error back to the caller
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}

Первое, что мы делаем, это инициализируем Diamond Storage. Здесь мы будем хранить данные нашего фасета. Далее, где находится разница в fallback()s. Мы смотрим diamond-2выше. Во всех трех мы проверяем, существует ли адрес фасета.
Вот как мы регистрируемся diamond-1:

// get facet from function selector
address facet = ds.facetAddressAndSelectorPosition[msg.sig].facetAddress;
if(facet == address(0)) {
revert FunctionNotFound(msg.sig);
}

Вот как мы регистрируемся diamond-3:

// get facet from function selector
address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress;
require(facet != address(0), "Diamond: Function does not exist");

Основное различие в трех реализациях заключается в том, что diamond-2мы храним селекторы в сопоставлении 32-байтовых слотов памяти.

Наконец, мы используем Yul для delegatecall()наших аспектов, копируя данные вызова, которые были отправлены на Diamond.sol, и проверяя, был ли вызов успешным.

На этом наш раздел о Diamond.sol. Далее мы рассмотрим, что происходило, когда мы звонили diamondCut().

DiamondCut.sol

Как мы уже обсуждали, DiamondCut.solотвечает за добавление, удаление и замену граней в нашем Алмазе. Все три реализации используют несколько иной подход, но достигают одной и той же цели. Давайте посмотрим, как diamond-1работает.

contract DiamondCutFacet is IDiamondCut {
/// @notice Add/replace/remove any number of functions and optionally execute/// @notice Add/replace/remove any number of functions and optionally execute
/// a function with delegatecall
/// @param _diamondCut Contains the facet addresses and function selectors
/// @param _init The address of the contract or facet to execute _calldata
/// @param _calldata A function call, including function selector and arguments
/// _calldata is executed with delegatecall on _init
function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external override {
LibDiamond.enforceIsContractOwner();
LibDiamond.diamondCut(_diamondCut, _init, _calldata);
}
}

Как видите, этот контракт сильно зависит от LibDiamond.sol. Все, что мы делаем, исходя из общего обзора, — это проверяем, что владелец контракта сделал этот вызов, а затем вызываем diamondCut(). Давайте посмотрим, что происходит в LibDiamond.sol. Прежде чем мы это сделаем, я должен указать на одну большую деталь о LibDiamond.sol. LibDiamond.solиспользует исключительно internalфункции. Это добавляет байт-код к нашему контракту, избавляя нас от необходимости использовать другой файл delegatecall().

Хорошо, теперь, когда мы понимаем, как сэкономить на топливе в LibDiamond.sol, давайте посмотрим на код diamondCut().

// Internal function version of diamondCut
function diamondCut(
IDiamondCut.FacetCut[] memory _diamondCut,
address _init,
bytes memory _calldata
) internal {
for (uint256 facetIndex; facetIndex < _diamondCut.length; facetIndex++) {
bytes4[] memory functionSelectors = _diamondCut[facetIndex].functionSelectors;
address facetAddress = _diamondCut[facetIndex].facetAddress;
if(functionSelectors.length == 0) {
revert NoSelectorsProvidedForFacetForCut(facetAddress);
}
IDiamondCut.FacetCutAction action = _diamondCut[facetIndex].action;
if (action == IDiamond.FacetCutAction.Add) {
addFunctions(facetAddress, functionSelectors);
} else if (action == IDiamond.FacetCutAction.Replace) {
replaceFunctions(facetAddress, functionSelectors);
} else if (action == IDiamond.FacetCutAction.Remove) {
removeFunctions(facetAddress, functionSelectors);
} else {
revert IncorrectFacetCutAction(uint8(action));
}
}
emit DiamondCut(_diamondCut, _init, _calldata);
initializeDiamondCut(_init, _calldata);
}

Мы берем те же параметры, что и раньше для этой функции. Сначала мы прокручиваем нашу FacetCutструктуру. Внутри цикла мы получаем наш селектор функций и адрес фасетной функции. Мы удостоверяемся, что селектор фасета действителен, и возвращаемся в противном случае. Затем нам нужно проверить действие, которое мы выполняем с этой конкретной функцией (добавить, заменить или удалить). Найдя действие, мы вызываем вспомогательную функцию, которая коррелирует с этим действием. Затем мы создаем событие, чтобы обеспечить прозрачность для наших пользователей. Наконец, мы проверяем, работает ли наш фасет, initializeDiamondCut()проверяя, имеет ли наш контракт код и может ли он быть вызван через delegatecall()with _calldata.

Теперь, когда мы знаем, как это diamondCut()работает, давайте посмотрим, как мы выполняем каждое действие, начинающееся с Add.

function addFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { addFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
if(_facetAddress == address(0)) {
revert CannotAddSelectorsToZeroAddress(_functionSelectors);
}
DiamondStorage storage ds = diamondStorage();
uint16 selectorCount = uint16(ds.selectors.length);
enforceHasContractCode(_facetAddress, "LibDiamondCut: Add facet has no code");
for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
bytes4 selector = _functionSelectors[selectorIndex];
address oldFacetAddress = ds.facetAddressAndSelectorPosition[selector].facetAddress;
if(oldFacetAddress != address(0)) {
revert CannotAddFunctionToDiamondThatAlreadyExists(selector);
}
ds.facetAddressAndSelectorPosition[selector] = FacetAddressAndSelectorPosition(_facetAddress, selectorCount);
ds.selectors.push(selector);
selectorCount++;
}
}

Входными параметрами являются адрес фасетного контракта и селектор конкретной функции, с которой мы работаем. Мы хотим убедиться, что этот контракт существует, проверив, является ли адрес нулевым адресом. Далее мы инициализируем Diamond Storage. Затем мы получаем количество селекторов, которые уже есть у нашего Алмаза. После этого мы проверяем, есть ли у нашего фасета код контракта. Теперь нам нужно убедиться, что этой функции еще нет в Diamond. Мы делаем это, перебирая селекторы функций и проверяя, существует ли уже адрес. В противном случае мы нажимаем наш селектор на наши сохраненные селекторы.

Теперь давайте посмотрим, как заменить грань!

function replaceFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { replaceFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
DiamondStorage storage ds = diamondStorage();
if(_facetAddress == address(0)) {
revert CannotReplaceFunctionsFromFacetWithZeroAddress(_functionSelectors);
}
enforceHasContractCode(_facetAddress, "LibDiamondCut: Replace facet has no code");
for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
bytes4 selector = _functionSelectors[selectorIndex];
address oldFacetAddress = ds.facetAddressAndSelectorPosition[selector].facetAddress;
// can't replace immutable functions — functions defined directly in the diamond in this case
if(oldFacetAddress == address(this)) {
revert CannotReplaceImmutableFunction(selector);
}
if(oldFacetAddress == _facetAddress) {
revert CannotReplaceFunctionWithTheSameFunctionFromTheSameFacet(selector);
}
if(oldFacetAddress == address(0)) {
revert CannotReplaceFunctionThatDoesNotExists(selector);
}
// replace old facet address
ds.facetAddressAndSelectorPosition[selector].facetAddress = _facetAddress;
}
}

Как вы могли заметить, Replaceзапускается так же, как Add. Мы инициализируем Diamond Storage, проверяем, имеет ли фасет допустимый адрес и размер кода, затем прокручиваем селекторы. Во-первых, мы проверяем, является ли селектор неизменным. Затем мы проверяем, является ли функция, которую мы хотим заменить, той же функцией, которую мы добавляем. После этого мы проверяем, действителен ли адрес фасета. В противном случае мы заменяем нашу грань.

Теперь давайте проверим наше последнее действие, Remove.

function removeFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { removeFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
DiamondStorage storage ds = diamondStorage();
uint256 selectorCount = ds.selectors.length;
if(_facetAddress != address(0)) {
revert RemoveFacetAddressMustBeZeroAddress(_facetAddress);
}
for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
bytes4 selector = _functionSelectors[selectorIndex];
FacetAddressAndSelectorPosition memory oldFacetAddressAndSelectorPosition = ds.facetAddressAndSelectorPosition[selector];
if(oldFacetAddressAndSelectorPosition.facetAddress == address(0)) {
revert CannotRemoveFunctionThatDoesNotExist(selector);
}

// can't remove immutable functions — functions defined directly in the diamond
if(oldFacetAddressAndSelectorPosition.facetAddress == address(this)) {
revert CannotRemoveImmutableFunction(selector);
}
// replace selector with last selector
selectorCount--;
if (oldFacetAddressAndSelectorPosition.selectorPosition != selectorCount) {
bytes4 lastSelector = ds.selectors[selectorCount];
ds.selectors[oldFacetAddressAndSelectorPosition.selectorPosition] = lastSelector;
ds.facetAddressAndSelectorPosition[lastSelector].selectorPosition = oldFacetAddressAndSelectorPosition.selectorPosition;
}
// delete last selector
ds.selectors.pop();
delete ds.facetAddressAndSelectorPosition[selector];
}
}

Опять же, мы начинаем с инициализации Diamond Storage, проверяя, имеет ли фасет действительный адрес, а затем прокручиваем селекторы. Затем мы получаем селектор функций и позицию в памяти. Мы проверяем, что он действительно существует, а затем проверяем, что он не является неизменным. После этого мы перемещаем наш селектор в конец массива и выполняем операцию pop()для его удаления.

diamond-2и diamond-3обе достигают той же цели, что и diamond-1функция diamondCut(), но используют другой синтаксис и архитектуру. Для простоты этой статьи мы не будем останавливаться на них. Однако, если достаточное количество людей будет заинтересовано в том, чтобы узнать, как они работают, я могу написать новую статью в будущем, в которой будут более подробно описаны различия в реализациях.

LoupeFacet.sol

Теперь, когда мы понимаем, как обновлять функции в нашем Алмазе, давайте рассмотрим, как мы можем просматривать наши грани. Помните, что for diamond-1и diamond-2не рекомендуется вызывать эти функции по цепочке. diamond-3, однако сильно оптимизирован для вызова этих функций в цепочке. Опять же, мы будем только повторяться diamond-1, но если вы понимаете, что происходит, вы должны быть в состоянии как понять, так и реализовать любую из других реализаций Diamond Standard.

Сначала мы рассмотрим facets(). facets(), возвращает все грани и их селекторы для Diamond.

function facets() external override view returns (Facet[] memory facets_) {facets() external override view returns (Facet[] memory facets_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
uint256 selectorCount = ds.selectors.length;
// create an array set to the maximum size possible
facets_ = new Facet[](selectorCount);
// create an array for counting the number of selectors for each facet
uint16[] memory numFacetSelectors = new uint16[](selectorCount);
// total number of facets
uint256 numFacets;
// loop through function selectors
for (uint256 selectorIndex; selectorIndex < selectorCount; selectorIndex++) {
bytes4 selector = ds.selectors[selectorIndex];
address facetAddress_ = ds.facetAddressAndSelectorPosition[selector].facetAddress;
bool continueLoop = false;
// find the functionSelectors array for selector and add selector to it
for (uint256 facetIndex; facetIndex < numFacets; facetIndex++) {
if (facets_[facetIndex].facetAddress == facetAddress_) {
facets_[facetIndex].functionSelectors[numFacetSelectors[facetIndex]] = selector;
numFacetSelectors[facetIndex]++;
continueLoop = true;
break;
}
}
// if functionSelectors array exists for selector then continue loop
if (continueLoop) {
continueLoop = false;
continue;
}
// create a new functionSelectors array for selector
facets_[numFacets].facetAddress = facetAddress_;
facets_[numFacets].functionSelectors = new bytes4[](selectorCount);
facets_[numFacets].functionSelectors[0] = selector;
numFacetSelectors[numFacets] = 1;
numFacets++;
}
for (uint256 facetIndex; facetIndex < numFacets; facetIndex++) {
uint256 numSelectors = numFacetSelectors[facetIndex];
bytes4[] memory selectors = facets_[facetIndex].functionSelectors;
// setting the number of selectors
assembly {
mstore(selectors, numSelectors)
}
}
// setting the number of facets
assembly {
mstore(facets_, numFacets)
}
}

Обратите внимание, что входных параметров нет, и мы указываем, что будем возвращать factes_. factes_представляет собой структуру данных, которая выглядит следующим образом:

struct Facet {
address facetAddress;address facetAddress;
bytes4[] functionSelectors;
}

Первое, что мы делаем внутри функции, это инициализируем Diamond Storage. Затем мы получаем количество имеющихся у нас селекторов. После этого мы инициализируем массив, который будем возвращать в конце нашей функции. Затем мы создаем массив для отслеживания количества функций на фасет и переменную для отслеживания количества фасетов. Затем мы прокручиваем наши селекторы функций. Внутри нашего цикла мы ищем, к какому аспекту принадлежит наш селектор. Нам нужно перебрать фасеты, чтобы найти, какой адрес соответствует адресу наших текущих селекторов функций. После того, как мы находим наш фасет, мы добавляем наш селектор функций в массив этого фасета, если он существует. В противном случае мы создаем массив. После того, как мы закончим наш цикл, мы еще раз пройдемся по граням. Внутри этого цикла мы сохраняем количество селекторов в памяти, чтобы вернуться позже. Окончательно, мы сохраняем количество граней и возвращаемся. Причина, по которой мы сохраняем количество селекторов и фасетов, заключается в том, что мы изначально инициализировали наши массивы до максимально возможного размера. Теперь, когда мы знаем, сколько конкретных селекторов и фасетов мы будем возвращать, мы сообщаем Solidity правильный размер возвращаемого массива.

Следующая функция, которую мы рассмотрим, это facetFunctionSelectors(). facetFunctionSelectors()возвращает селекторы функций для определенного аспекта. Он принимает в качестве параметра адрес целевого фасета и возвращает массив, bytes4[]представляющий селекторы функций.

function facetFunctionSelectors(address _facet) external override view returns (bytes4[] memory _facetFunctionSelectors) {facetFunctionSelectors(address _facet) external override view returns (bytes4[] memory _facetFunctionSelectors) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
uint256 selectorCount = ds.selectors.length;
uint256 numSelectors;
_facetFunctionSelectors = new bytes4[](selectorCount);
// loop through function selectors
for (uint256 selectorIndex; selectorIndex < selectorCount; selectorIndex++) {
bytes4 selector = ds.selectors[selectorIndex];
address facetAddress_ = ds.facetAddressAndSelectorPosition[selector].facetAddress;
if (_facet == facetAddress_) {
_facetFunctionSelectors[numSelectors] = selector;
numSelectors++;
}
}
// Set the number of selectors in the array
assembly {
mstore(_facetFunctionSelectors, numSelectors)
}
}

Снова инициализируем Diamond Storage. Затем мы получаем количество селекторов функций и инициализируем наш возвращаемый массив. Далее мы прокручиваем селекторы. Здесь мы проверяем, совпадает ли адрес селектора с нашим целевым аспектом. Если это так, мы сохраняем этот селектор функций. Наконец, мы сохраняем количество селекторов и возвращаемся.

Теперь рассмотрим facetAddresses(), который возвращает массив адресов граней нашего Алмаза.

function facetAddresses() external override view returns (address[] memory facetAddresses_) {facetAddresses() external override view returns (address[] memory facetAddresses_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
uint256 selectorCount = ds.selectors.length;
// create an array set to the maximum size possible
facetAddresses_ = new address[](selectorCount);
uint256 numFacets;
// loop through function selectors
for (uint256 selectorIndex; selectorIndex < selectorCount; selectorIndex++) {
bytes4 selector = ds.selectors[selectorIndex];
address facetAddress_ = ds.facetAddressAndSelectorPosition[selector].facetAddress;
bool continueLoop = false;
// see if we have collected the address already and break out of loop if we have
for (uint256 facetIndex; facetIndex < numFacets; facetIndex++) {
if (facetAddress_ == facetAddresses_[facetIndex]) {
continueLoop = true;
break;
}
}
// continue loop if we already have the address
if (continueLoop) {
continueLoop = false;
continue;
}
// include address
facetAddresses_[numFacets] = facetAddress_;
numFacets++;
}
// Set the number of facet addresses in the array
assembly {
mstore(facetAddresses_, numFacets)
}
}

Опять же, первое, что мы делаем, это инициализируем Diamond Storage, получаем количество селекторов функций и инициализируем наш возвращаемый массив. Мы также снова просматриваем наши селекторы. Мы проверяем адрес фасета нашего селектора. Затем мы проверяем, видели ли мы этот адрес раньше. Если у нас есть, мы пропускаем эту итерацию цикла. В противном случае мы добавляем этот новый адрес в наш возвращаемый массив. Опять же, мы обновляем размер массива и возвращаем его.

Последняя функция, которую мы рассмотрим, DiamondLoupeFacet.solэто facetAddress. Эта функция возвращает адрес фасета, предоставленного селектором функции.

function facetAddress(bytes4 _functionSelector) external override view returns (address facetAddress_) {facetAddress(bytes4 _functionSelector) external override view returns (address facetAddress_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddress_ = ds.facetAddressAndSelectorPosition[_functionSelector].facetAddress;
}

Эта функция довольно проста. Мы инициализируем Diamond Storage и используем то, как мы храним наши селекторы, чтобы вернуть наш адрес фасета.

На этом мы завершаем наш раздел, посвященный аспекту лупы. Теперь мы знаем, как Diamond Standard обеспечивает прозрачность для своих пользователей. Далее мы рассмотрим, как работает развертывание Diamond Standard.

Как развернуть Diamond Standard

При развертывании Diamond вам необходимо развернуть фасеты, а затем развернуть файлы Diamond.sol. Таким образом, вы можете сообщить своему Бриллианту, какие контракты он будет вызывать. Самый простой способ развернуть Diamond — с помощью пакета npm diamond-util. Вот как его установить:

npm i diamond-utili diamond-util

После его установки вы можете развернуть свой Diamond с помощью следующего кода.

// eslint-disable-next-line no-unused-vars
const deployedDiamond = await diamond.deploy({
diamondName: 'LeaseDiamond',
facets: [
'DiamondCutFacet',
'DiamondLoupeFacet',
'Facet1',
'Facet2'
],
args: [/* your parameters */]
})

// init facets
const diamondCutFacet = await ethers.getContractAt('DiamondCutFacet', deployedDiamond.address)
const diamondLoupeFacet = await ethers.getContractAt('DiamondLoupeFacet', deployedDiamond.address)
const facet1 = await ethers.getContractAt('Facet1', deployedDiamond.address)
const facet2 = await ethers.getContractAt('Facet2', deployedDiamond.address)

Эта библиотека берет на себя большую часть работы за нас! Все, что нам нужно сделать, это перечислить наши аспекты, и библиотека развернет их вместе с бриллиантом. Обратите внимание, что когда мы инициализируем наши контракты, ethers.jsмы устанавливаем адрес на Diamond.solадрес .

Если вы хотите добавить новый аспект, вы можете сделать это, вызвав DiamondCutFacet.sol. Вот пример.

const FacetCutAction = { Add: 0, Replace: 1, Remove: 2 }

const Facet3 = await ethers.getContractFactory('Facet3')
const facet3 = await Facet3.deploy()
await facet3.deployed()
const selectors = getSelectors(facet3).remove(['supportsInterface(bytes4)'])
tx = await diamondCutFacet.diamondCut(
[{
facetAddress: facet3.address,
action: FacetCutAction.Add,
functionSelectors: selectors
}],
ethers.constants.AddressZero, '0x', { gasLimit: 800000 }
)

receipt = await tx.wait()

Как видите, сначала мы развертываем наш фасет. Затем мы используем наш пакет npm для получения селекторов функций. Затем мы вызываем DiamondCutFacet.solобновить наш Diamond. Replaceработает аналогично, за исключением того, что вы должны убедиться, что заменяемые селекторы уже есть в Diamond. Remove, также работает аналогично, но убедитесь, что вы передаете именно те селекторы, которые хотите удалить.

Поздравляем! Теперь вы знаете, как создать и развернуть приложение блокчейна с помощью Diamond Standard!

Заключение

На этом моя статья об Diamond Standard заканчивается. Надеюсь, я помог вам понять сложности и способы его реализации в ваших собственных проектах.