Аннотация:
Алмазный паттерн — это довольно свежий стандарт, который предлагает решение многих проблем солидности:
- Один адрес для нескольких контрактов
- Вы можете обновлять свои функции после развертывания контракта
- Нет ограничения на размер 24 кб
- …но он также имеет некоторые недостатки или, скажем так, компромиссы.Один из этих компромиссов заключается в том, что вы не можете иметь несколько функций с одинаковой площадью внутри одного алмаза. Это может усложнить ситуацию, когда вы хотите иметь несколько токенов под одним алмазом (например, контракт ERC721 NFT с контрактом ERC20 токенов).Чтобы узнать больше о стандарте Diamonds, проверьте: https://github.com/mudgen/awesome-diamondsI Я наткнулся на этот паттерн, потому что мне нужна была какая-то архитектура прокси для масштабирования контрактов Parcels (Parcels — это игра & компания, соучредителем которой я являюсь: https://twitter.com/parcelsgame).## ПроблемаДайте мне продемонстрировать то, о чем я говорил выше.Это невозможно:
//FacetA.sol
contract FacetA {
function my_function(uint256 number) external {
//... Some logic
}
}
//FacetB.sol
contract FacetB {
function my_function(uint256 number) external {
//... Some other logic, with the same footprint
}
}
Поскольку все функции facets будут открыты под одним единственным адресом контракта (адресом бриллианта), он не мог определить, какую из них использовать при вызове diamond.my_function(number)
.
Для этого примера можно было бы просто поменять следы, например, вызывать функции соответственно functionA
и functionB
, но часто для реализации стандартов и интерфейсов требуется предварительный след. Это может произойти, например, с erc20 и erc721, которые имеют одну и ту же функцию balanceOf(address _owner)
.
Тем не менее, это возможно:
//FacetA.sol
contract FacetA {
function function(uint256 number) external {
//...
}
}
//FacetB.sol
contract FacetB {
function function(uint256 number, string str) external {
//...
}
}
Потому что обе функции теперь имеют разный след из-за аргументов, даже под общим именем. Для тех, кто знаком с объектно-ориентированным программированием, это очень похоже на то, как в большинстве языков можно объявить несколько конструкторов.
Это означает, что вы не можете иметь несколько фасетов, реализующих один и тот же интерфейс.
Но это идет еще дальше, поскольку различные стандарты могут частично противоречить друг другу, например, ERC20 и ERC721, что означает, что вы не можете реализовать оба одновременно внутри общего алмаза.
Стандарт ERC1155
К счастью, существует стандарт, который разработан для работы с несколькими токенами на одном контракте: ERC1155.
По сути, ERC1155 хранит балансы за двумя параметрами: id и адрес. Именно этот id позволяет нам иметь несколько токенов одновременно на этом контракте.
Эти токены могут быть сменными (если вы разрешаете майнить более одного) или несменными (если вы разрешаете майнить только один токен на один id).
Чтобы разграничить различные типы токенов, вы можете разделить идентификаторы на диапазоны:
Допустим, нам нужно приложение, в котором у вас есть один токен $GLD (который обычно реализуется сменным токеном ERC20) и 1000 уникальных майнеров (которые обычно реализуются не сменным токеном ERC721). Чтобы реализовать эти два токена в рамках нашего контракта ERC1155, нам нужно разделить идентификаторы: 1 для токена $GLD и 1000 для NFT майнеров. Здесь мы можем просто присвоить первый id (0) токену $GLD, а id 1 — 1001 присвоить майнерам.
В более общем случае вы можете определить «базовую константу» для каждого типа токенов, так что здесь у вас будет что-то вроде:
uint256 constant GLD_ID = 0;
uint256 constant MINER_BASE_ID = 1;
Чтобы вы могли получить доступ к шахтеру №x
с помощью balance(MINER_BASE_ID + x)
.
Если вам нужно несколько широких диапазонов (например, два типа нфс), вы можете установить базовый id, сдвигая биты:
uint256 constant GLD_ID = 0;
uint256 constant MINER_BASE_ID = 1;
uint256 constant OTHERNFT_BASE_ID = 1 <<< 128;
Более подробную информацию о стандарте ERC1155 можно найти на сайте: https://eips.ethereum.org/EIPS/eip-1155.
Архитектура
Для реализации ERC1155 я советую solidstate-solidity LINK, совместимый со стандартом diamond, или адаптацию OpenZepplin’овских, а не переписывание собственных с нуля, что может добавить уязвимости.
Итак, теперь у нас есть четкий способ создания нескольких токенов с помощью нашего контракта ERC1155, но наличие всех функций в одном контракте для каждого типа токенов привело бы к беспорядочному коду, и, что еще хуже, могло бы быть даже слишком много.
Чтобы решить эту проблему, мы разделим логику каждого токена в отдельном контракте Facet (см. паттерн diamond для понимания фасетов).
Но мы, вероятно, захотим вызывать функции из одного контракта в другой (например, вы можете захотеть получить доступ к балансу $GLD для обновления майнера). Хотя это возможно, если оставить логику в разделенных фасетах, лучшим вариантом является перемещение всей логики в библиотеки, что упрощает вызовы. Это также позволит оставить только внешние функции getters / setters в Facets, что сделает архитектуру еще чище, поскольку внешняя логика будет отделена от внутренней логики (соответственно Facets & Libraries).
Простая архитектура токен ($GLD) + NFT (Miner) будет выглядеть примерно так:
- Фасеты открывают только внешние функции: ERC1155Facet для стандартных функций ERC1155, TokenFacet & NFTFacet для пользовательских функций (например,
getGLDBalance(address of)
). - Библиотеки LibToken & LibNFT обрабатывают всю логику (которая будет использоваться как внутренняя и внешняя), чтобы ее можно было повторно использовать в фасетах & других библиотеках.
- LibStorage содержит все данные, которые должны быть сохранены, и которые не обрабатываются ERC1155, см. шаблон AppStorage / DiamondStorage.
- LibERC1155Internal — это копия функций, определенных в контракте Solidstate
ERC1155Internal.sol
, чтобы мы могли вызывать внутренние функции из разных фасетов и библиотек (например, _mint и т.д.). Вам просто нужно добавить события из интерфейсаIERC1155Internal.sol
и убрать ключевые словаvirtual
из функций (так как библиотечные функции не могут / не будут перезаписаны). See https://gist.github.com/nohehf/3a1116e47d932bb9477bbc5332e61a9a .
Такая архитектура делает совместное использование логики в различных частях приложения беспроблемным, сохраняя проблемы в отдельных кодовых базах.
Примеры сниппетов:
Итак, для нашего примера $GLD / Miner у нас будет следующая структура (основанная на стартере hardhat-diamond-3):
contracts
├── Diamond.sol
├── facets
│ ├── DiamondCutFacet.sol
│ ├── DiamondLoupeFacet.sol
│ ├── ERC1155Facet.sol 🔺
│ ├── GLDFacet.sol 🔺
│ ├── MinerFacet.sol 🔺
│ └── OwnershipFacet.sol
├── interfaces
│ ├── IDiamondCut.sol
│ ├── IDiamondLoupe.sol
│ ├── IERC165.sol
│ └── IERC173.sol
├── libraries
│ ├── LibDiamond.sol
│ ├── LibERC1155Internal.sol 🔺
│ ├── LibGLD.sol 🔺
│ ├── LibMiner.sol 🔺
│ └── LibStorage.sol 🔺
└── upgradeInitializers
└── DiamondInit.sol
Только файлы, отмеченные 🔺, являются пользовательскими, остальные предоставляются из стартового репозитория
ERC1155Facet.sol:
// SPDX-License-Identifier: UNLICENCED
pragma solidity ^0.8.9;
import {ERC1155} from "@solidstate/contracts/token/ERC1155/ERC1155.sol";
contract ERC1155Facet is ERC1155 {}
GLDFacet.sol:
// SPDX-License-Identifier: UNLICENCED
pragma solidity ^0.8.9;
import "../libraries/LibGLD.sol";
import {LibStorage, AppStorage, ArtefactType} from "../libraries/LibStorage.sol";
import {Modifiers} from "../libraries/LibStorage.sol";
contract ArtefactFacet is Modifiers {
// ----- GETTERS -----
function getMyGLDBalance(address addr) external view returns (uint256) {
return LibGLD._getBalance(msg.sender);
}
// ...
// ----- SETTERS -----
// ...
}
LibERC1155Internal.sol (см. https://gist.github.com/nohehf/3a1116e47d932bb9477bbc5332e61a9a)
LibGLD.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import {LibStorage, AppStorage, ArtefactType} from "./LibStorage.sol";
import "@solidstate/contracts/token/ERC1155/base/ERC1155BaseStorage.sol";
// import {ERC1155Facet} from "../facets/ERC1155Facet.sol";
import "../facets/ERC1155Facet.sol";
import "./LibERC1155Internal.sol";
// Handles all the $GLD token logic
library LibGLD {
//CONSTANTS
uint256 constant GLD_ID = 0;
//STORAGE GETTERS:
// common storage
function s() internal pure returns (AppStorage storage) {
return LibStorage.diamondStorage();
}
//erc1155 storage (NOTE: you should prefer calling LibERC1155, but it can be usefull)
function s1155() internal pure returns (ERC1155BaseStorage.Layout storage) {
return ERC1155BaseStorage.layout();
}
//GLD LOGIC
function _getBalance(address addr) internal view returns (uint256) {
return LibERC1155Internal._balanceOf(addr, GLD_ID);
}
function _mint(address to, uint256 amount) internal {
LibERC1155Internal._mint(to, GLD_ID, amount, "");
}
// ...
}
Обратите внимание, что я привел примеры только для майнера, который будет довольно похож на GLD.
Заключение
Помимо устранения проблемы коллизии отпечатков между несколькими токенами под одним алмазом, этот solitions обеспечивает масштабируемое, простое в использовании и тестировании решение для мульти-токенов Dapps. Вызов внутренних функций без необходимости полагаться на развернутый адрес контракта очень удобен (и дешевле). После создания этот шаблон позволил мне работать плавно и легко внедрять новые функции для каждого из моих токенов, сохраняя чистый код и каталоги.
Возможные улучшения:
-> Solidstate-solidity может перенести всю логику в Libs, чтобы нам не пришлось все копировать-вставлять.
-> Стандарт Diamonds должен удалить реализацию ERC165 на DiamondLoupeFacet
, которую мы сейчас должны удалить, чтобы добавить грань ERC1155 (что IMO немного проблематично).
-> Стартовое репо для ERC1155 & Diamonds.
Я также обсуждаю с создателями solidstate & diamonds возможность улучшить их документацию, или даже
сделать настоящий фреймворк с solidstate-solidity & diamonds вместе с пошаговым руководством и документацией, поддерживающий нативно эти типы архитектур.
Спасибо, что прочитали, и, пожалуйста, спросите меня в twitter, если хотите узнать больше подробностей / хотите внести свой вклад: www.twitter.com/nohehf .