{"content":{"title":"如何实施钻石标准","body":"The Diamond Standard，EIP-2535，由 Nick Mudge 创建，作为实现代理智能合约的标准化架构。在本文中，我们将讨论使用 Diamond Standard 的优点以及其工作原理。\r\n\r\n## 总体概述\r\n\r\nDiamond Standard 的运作是通过部署一个名为 `Diamond.sol` 的合约。`Diamond.sol` 随后通过 `delegatecall()` 调用其他智能合约。这使得 `Diamond.sol` 可以在 `Diamond.sol` 的上下文中执行被调用合约的代码。所有通过 `delegatecall()` 被调用的合约都称为 facets。随着时间的推移，facets 可以被替换、移除和添加，这使得开发者能够在与 EVM 兼容的区块链上创建模块化应用程序。Diamond Standard 要求你实现 `DiamondLoupeFacet.sol`。`DiamondLoupeFacet.sol` 负责记录你协议中的其他 facets。`DiamondLoupeFacet.sol` 通过允许用户查看 facet 地址和功能提供透明度。Diamond Standard 的另一个要求是 `DiamondCutFacet.sol`。`DiamondCutFacet.sol` 负责所有应用程序的升级。当调用时，`DiamondCutFacet.sol` 还会发出事件，为协议用户提供另一层透明度。虽然不一定是要求，`LibDiamond.sol` 是一个库，提供许多帮助函数，以便使用 Diamond Standard 编写应用程序。在使用 Diamond Standard 创建应用程序时，你可以选择两条路径来维护你的状态变量。它们被称为 `App Storage` 和 `Diamond Storage`。在接下来的文章中，我们将详细讨论 Diamond Standard 的所有组件。\r\n\r\nDiamond Standard 具有三种不同的实现版本。幸运的是，如果你决定选择了错误的实现，可以将应用程序升级到不同的实现。`diamond-1` 是最基本的实现。`diamond-1` 的复杂性是最容易理解的，其 gas 成本也最轻。由于 gas 成本高昂，不建议在链上调用 `DiamondLoupeFacet.sol` 的函数（或 `diamond-2`）。另一方面，`diamond-3` 选择优化在链上调用 `DiamondLoupeFacet.sol` 的函数。其权衡在于调用 `DiamondCutFacet.sol` 的成本更高。`diamond-2` 与 `diamond-1` 非常相似，但在复杂性上优先考虑 gas 成本。作为开发者，你需要决定这三种实现中哪个最适合你的应用程序。\r\n\r\n由于 Diamond Standard 的复杂性，建议在创建应用程序时遵循一般模板。我会在文章底部链接这些模板以及进一步阅读的材料。\r\n\r\n## 为什么使用 Diamond Standard？\r\n\r\n对 Diamond Standard 的主要批评是其复杂性（希望我能帮助解决这个问题）。尽管它复杂，Diamond Standard 提供了以下好处：\r\n\r\n- 你的应用程序在实际大小上没有限制。所有智能合约的最大大小为 24KB。由于 Diamond Standard 使用 `Diamond.sol` 来 `delegatecall()` 它的 facets，合约的大小保持相对较小。\r\n- 你只需使用一个地址来实现多种功能。再次感谢 `delegatecall()`，你可以将所有希望的功能添加到单个智能合约中。这包括用与你的协议相同的地址实现代币。\r\n- 提供了一种有条理的方式来升级你的智能合约。这可以帮助审核员理解和保护你的应用程序。\r\n- 允许你逐步升级你的智能合约。你不必重新部署整个合约，只需添加、替换或移除一个 facet 即可为你的应用程序提供所需的功能。\r\n- 为你的代理合约提供了很高的透明度。正如前面提到的，`DiamondLoupeFacet.sol` 允许用户查看你的函数在区块链上的位置以及这些函数执行的操作。请查阅 [https://louper.dev/](https://louper.dev/) 以分析利用 Diamond Standard 的智能合约的功能。\r\n- 对于你应用程序中可以使用的 facets 的数量几乎没有限制。你无法添加更多 facets 到你的协议的唯一原因会是 `Diamond.sol` 没有足够的存储空间存放 facet 数据。这需要相当多的 facets，实现几乎不可能。\r\n- facets 可以被多个 `Diamond.sol` 合约重用。这样可以在部署借用功能的应用程序时节省 gas 成本。\r\n- 如果你愿意，可以在更高的运行设置下运行优化器。Solidity 优化器有助于优化调用外部函数时的 gas 成本。但这样会增加合约部署字节码。这可能会导致你的智能合约超出最大合约大小限制。由于你可以部署任意数量的 facets，因此可以将优化器设置为你希望的尽可能高的运行次数，而无需担心合约太大。\r\n\r\n有许多原因你可能希望在应用程序中使用 Diamond Standard，但请记住不要使你的应用程序过于复杂。如果你的协议可以使用单个智能合约，并且上述优点不适用于你的应用程序，则没有必要使用 Diamond Standard。它只会带来更多的复杂性和 gas 成本。但是，如果你面临需要为你的应用程序提供代理模式的情况，我个人推荐使用 Diamond Standard。\r\n\r\n## App Storage 和 Diamond Storage\r\n\r\n状态变量的架构是你的区块链应用程序最重要的方面之一。我决定首先介绍这一部分，以便让你充分了解 Diamond Standard 如何管理状态变量。显然，代理在 `delegatecall()` 上依赖很大，以便在主合约（`Diamond.sol`）的上下文中执行来自你的 facet 合约的代码。由于我们的状态变量存储全部保留在 `Diamond.sol` 中，我们需要确保我们的变量不会相互覆盖。在查看我们组织状态变量的正确方法之前，让我们先看一个错误管理状态变量的例子。\r\n\r\n```\r\npragma solidity^0.8.17;\r\n\r\ncontract Main {\r\n    uint256 public verySpecialVar;\r\n    uint256 public notSoSpecialVar;\r\n    // delegate calls SpecialVarManager 来更新 verySpecialVar\r\n    function setVerySpecialVar(address _specialVarManager) external {\r\n        _specialVarManager.delegatecall(\r\n            abi.encodeWithSignature(\"writeSpecialVar()\")\r\n        );\r\n    }\r\n    // delegate calls NotSpecialVarManager 来更新 notSoSpecialVar\r\n    function setNotSoSpecialVar(address _notSpecialVarManager) external {\r\n        _notSpecialVarManager.delegatecall(\r\n            abi.encodeWithSignature(\"writeNotSpecialVar()\")\r\n        );\r\n    }\r\n\r\n}\r\ncontract SpecialVarManager {\r\n    uint256 verySpecialVar;\r\n    function writeSpecialVar() external {\r\n        verySpecialVar = 100;\r\n    }\r\n}\r\ncontract NotSpecialVarManager {\r\n    uint256 notSoSpecialVar;\r\n    function writeNotSpecialVar() external {\r\n        notSoSpecialVar = 50;\r\n    }\r\n}\r\n```\r\n\r\n如果你调用 `setVerySpecialVar()`，你会看到 `verySpecialVar` 已被更新为 100！现在让我们调用 `setNotSoSpecialVar` 来看看会发生什么。我们会发现 `notSoSpecialVar` 仍然没有初始化，等于 0。如果我们检查 `verySpecialVar`，我们会发现它现在被设为 50。为什么？因为 `NotSpecialVarManager` 中的存储布局没有 `verySpecialVar`。所以当我们通过 `delegatecall()` 调用 `writeNotSpecialVar()` 时，我们告诉 `Main` 将存储槽 0 更新为 50。Solidity 并不关心你命名变量的方式；它只关注存储槽的位置。\r\n\r\n考虑到这一点，我们需要一种方式来组织我们的状态变量，以免覆盖存储槽。第一种方法是 Diamond Storage。\r\n\r\nDiamond Storage 利用 Solidity 智能合约中有多少存储槽（2²⁵⁶）。Diamond Storage 的理论是，由于存储槽数量庞大，如果我们哈希一个唯一值，我们将得到一个几乎肯定不会与其他存储槽冲突的随机存储槽。这听起来可能风险很大，但实际上这与 Solidity 用于存储映射和动态数组的方式是一样的。Diamond Storage 为你的 facets 提供了保持特定于其合约的状态变量的机会，同时也允许 facets 共享状态变量（如果需要）。\r\n\r\n由于 Diamond Standard 的复杂性需要一些设置，对于这些存储示例，我不会使用 Diamond Standard。这一部分的主要目的是理解你的状态变量如何存储。在 Diamond Standard 中的实现大致相同。\r\n\r\n```\r\npragma solidity^0.8.17;\r\n\r\nlibrary SharedLib {\r\n    // 结构体带有状态变量\r\n    struct DiamondStorage {\r\n        uint256 sharedVar;\r\n    }\r\n    // 返回带有状态变量的存储变量\r\n    function diamondStorage() internal pure returns(DiamondStorage storage ds) {\r\n        // 通过哈希字符串获取“随机”存储位置\r\n        bytes32 storagePosition = keccak256(abi.encode(\"Diamond.Storage.SharedLib\"));\r\n        // 将我们的结构体存储槽分配到存储位置\r\n        assembly {\r\n            ds.slot := storagePosition\r\n        }\r\n\r\n    }\r\n}\r\n// 不是实际的 Diamond Standard\r\ncontract PseudoDiamond {\r\n    // delegate calls Facet1 来更新 sharedVar\r\n    function writeToSharedVar(address _facet1, uint256 _value) external {\r\n        // 通过 delegatecall 写入\r\n        _facet1.delegatecall(\r\n            abi.encodeWithSignature(\"writeShared(uint256)\", _value)\r\n        );\r\n    }\r\n    // delegate calls Facet2 来读取 sharedVar\r\n    function readSharedVar(address _facet2) external returns (uint256) {\r\n        // 返回 delegate call 的结果\r\n        (bool success, bytes memory _valueBytes) = _facet2.delegatecall(\r\n            abi.encodeWithSignature(\"readShared()\")\r\n        );\r\n        // 由于返回值是字节数组，我们使用汇编来检索我们的 uint\r\n        bytes32 _value;\r\n        assembly {\r\n            let location := _valueBytes\r\n            _value := mload( add(location, 0x20) )\r\n        }\r\n        return uint256(_value);\r\n    }\r\n}\r\ncontract Facet1 {\r\n    function writeShared(uint256 _value) external {\r\n        // 通过调用库函数初始化存储结构\r\n        SharedLib.DiamondStorage storage ds = SharedLib.diamondStorage();\r\n        // 写入共享变量\r\n        ds.sharedVar = _value;\r\n\r\n    }\r\n}\r\ncontract Facet2 {\r\n\r\n    function readShared() external view returns (uint256) {\r\n        // 通过调用库函数初始化存储结构\r\n        SharedLib.DiamondStorage storage ds = SharedLib.diamondStorage();\r\n\r\n        // 返回共享变量\r\n        return ds.sharedVar;\r\n\r\n    }\r\n}\r\n```\r\n\r\n如果你调用 `PseudoDiamond.writeSharedVar()`，然后 `PseudoDiamond.readSharedVar()`，你将看到你的值。通过使用库并索引一个“随机”的存储槽，我们可以在两个智能合约之间共享变量。当我们调用 `delegatecall()` 两个 facets 时，它正在查看 `DiamondStorage` 结构的存储位置以访问该变量。通过之间关于我们想在哪里存储数据的明确沟通，我们防止了状态变量的冲突。如果你想为仅一个 facet 拥有状态变量，你可以简单地创建一个库，类似于 `SharedLib`，并仅将其实现到特定的 facet 中。\r\n\r\nApp Storage 的工作方式稍微不同。你创建一个 Solidity 文件，在该文件中创建一个名为 `AppStorage` 的结构体。然后你可以在该结构体中放置任意多的状态变量，包括其他结构体。然后在你的智能合约中，你执行的第一件事情就是初始化 `AppStorage`。这就是将存储槽 0 设置到结构体开始的位置。这在合约之间创建了一个人类可读的共享状态。让我们看一个示例！\r\n\r\n```\r\npragma solidity^0.8.17;\r\n\r\n// 结构体带有状态变量\r\nstruct StateVars {\r\n    uint256 sharedVar;\r\n}\r\n// 我们的应用存储结构体\r\nstruct AppStorage {\r\n    StateVars state;\r\n}\r\n// 不是实际的 Diamond Standard\r\ncontract PseudoDiamond {\r\n    AppStorage s;\r\n    // delegate calls Facet1 来更新 sharedVar\r\n    function writeToSharedVar(address _facet1, uint256 _value) external {\r\n        // 通过 delegate call 写入\r\n        _facet1.delegatecall(\r\n            abi.encodeWithSignature(\"writeShared(uint256)\", _value)\r\n        );\r\n    }\r\n    // delegate calls Facet2 来读取 sharedVar\r\n    function readSharedVar(address _facet2) external returns (uint256) {\r\n        // 返回 delegate call 的结果\r\n        (bool success, bytes memory _valueBytes) = _facet2.delegatecall(\r\n            abi.encodeWithSignature(\"readShared()\")\r\n        );\r\n        // 由于返回值是字节数组，我们使用汇编来检索我们的 uint\r\n        bytes32 _value;\r\n        assembly {\r\n            let location := _valueBytes\r\n            _value := mload( add(location, 0x20) )\r\n        }\r\n        return uint256(_value);\r\n    }\r\n}\r\ncontract Facet1 {\r\n    AppStorage s;\r\n    function writeShared(uint256 _value) external {\r\n        // 写入我们的存储中的状态变量\r\n        s.state.sharedVar = _value;\r\n    }\r\n}\r\ncontract Facet2 {\r\n    AppStorage s;\r\n\r\n    function readShared() external view returns (uint256) {\r\n        // 从应用存储返回状态变量\r\n        return s.state.sharedVar;\r\n    }\r\n}\r\n```\r\n\r\n如果你查看 `PseudoDiamond` 的存储槽 0 中的值，你会看到 100，这是我们的值！关于 App Storage 的一个重要说明是，如果你需要在部署后更新你的 AppStorage，请确保在 AppStorage 的末尾添加新的状态变量，以防止存储冲突。我个人更喜欢 App Storage 而不是 Diamond Storage，因为组织性更好，但两者都能完成工作。也值得注意的是，Diamond Storage 和 App Storage 不是互斥的。即使你使用 App Storage 来管理你的状态变量，`LibDiamond.sol` 也使用 Diamond Storage 来管理 facet 数据。\r\n\r\n## Diamond.sol\r\n\r\n正如前面提到的，`Diamond.sol` 是在与你的应用程序交互时被调用的智能合约。如果要这样理解它，将 `Diamond.sol` 视为“管理”你整个应用程序的合约。所有 facet 函数将显得是 `Diamond.sol` 自身的函数。\r\n\r\n让我们首先查看 `diamond-1` 的构造函数中发生了什么。\r\n\r\n```\r\n// 这在 diamond 构造函数中使用\r\n// 更多参数被添加到这个结构体中\r\n// 这避免堆栈太深的错误\r\nstruct DiamondArgs {\r\n    address owner;\r\n    address init;\r\n    bytes initCalldata;\r\n}\r\n\r\ncontract Diamond {\r\n    constructor(IDiamondCut.FacetCut[] memory _diamondCut, DiamondArgs memory _args) payable {\r\n        LibDiamond.setContractOwner(_args.owner);\r\n        LibDiamond.diamondCut(_diamondCut, _args.init, _args.initCalldata);\r\n        // 可以在这里添加代码以执行操作和设置状态变量。\r\n    }\r\n    // 代码的其余部分\r\n}\r\n```\r\n\r\n首先，在合约外部，我们有一个格式化我们数据的结构体。接下来，我们将该结构体作为参数与看起来像这样的另一个结构体一起传递：\r\n\r\n```\r\nstruct FacetCut {\r\n    address facetAddress;\r\n    FacetCutAction action;\r\n    bytes4[] functionSelectors;\r\n}\r\n```\r\n\r\n`FacetCut` 提供了 facet 的地址、我们想要在 facet 上执行的操作（添加、替换或移除）以及该 facet 的函数的选择器。接下来，我们设置合约所有者，并添加提交给 Diamond 的任何 facet 数据。我们稍后会更详细地讨论这段代码如何工作。之后，如果我们愿意，我们可以初始化状态变量。请记住，你永远不要在 facet 的构造函数中执行任何状态变量的赋值，因为它将在 facet 智能合约内部而不是 `Diamond.sol` 执行。\r\n\r\n到目前为止，这个看起来相当直接，因此让我们看看 `diamond-2` 和 `diamond-3` 的构造函数中发生的事情。\r\n\r\n```\r\ncontract Diamond {\r\n\r\n  constructor(address _contractOwner, address _diamondCutFacet) payable {\r\n        LibDiamond.setContractOwner(_contractOwner);\r\n        // 添加 diamondCutFacet 中的 diamondCut 外部函数\r\n        IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1);\r\n        bytes4[] memory functionSelectors = new bytes4[](1);\r\n        functionSelectors[0] = IDiamondCut.diamondCut.selector;\r\n        cut[0] = IDiamondCut.FacetCut({\r\n            facetAddress: _diamondCutFacet,\r\n            action: IDiamondCut.FacetCutAction.Add,\r\n            functionSelectors: functionSelectors\r\n        });\r\n        LibDiamond.diamondCut(cut, address(0), \"\");\r\n    }\r\n    // 代码的其余部分\r\n}\r\n```\r\n\r\n这次我们只传入合约的所有者和 `DiamondCutFacet.sol` 的地址。接下来，我们为 Diamond 指定所有者。在此之后，我们添加 `diamondCut()` 函数，以便我们可以添加更多的 facets。通过首先初始化一个结构体类型为 `FacetCut` 的长度为 1 的内存数组来实现。然后，我们初始化另一个元素为 1 的数组。这次这个数组是 `bytes4`，用来存储 `diamondCut()` 的函数选择器。接下来，我们通过调用 Diamond Cut 接口的 `diamondCut` 函数并获取其选择器来将选择器赋值给数组。最终，我们可以分配 `cut[0]`。我们使用 `DiamondCutFacet.sol` 的地址，添加（因为我们想将这个 facet 添加到我们的 Diamond 中）以及选择器数组。最后，我们实际上添加了 `diamondCut`。这在不知道底层发生了什么的情况下显得相当多，因此如果你觉得有帮助，可以在我们讨论 `DiamondCut.sol` 后重新阅读这一部分。目前，仅需了解我们在构造函数中添加了 facets。\r\n\r\n`Diamond.sol` 的最后部分是 `fallback()` 函数。这是我们如何使用 Diamond Standard 调用我们的 facets 的方式。它在所有三种实现中几乎相同，所以让我们来看看！\r\n\r\n```\r\n// 查找被调用的函数的 facet，并执行该函数\r\n// 如果找到了 facet，则执行函数并返回任何值。\r\nfallback() external payable {\r\n    LibDiamond.DiamondStorage storage ds;\r\n    bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION;\r\n    // 获取 diamond storage\r\n    assembly {\r\n        ds.slot := position\r\n    }\r\n    // 从函数选择器获取 facet\r\n    address facet = address(bytes20(ds.facets[msg.sig]));\r\n    require(facet != address(0), \"Diamond: Function does not exist\");\r\n    // 使用 delegatecall 从 facet 执行外部函数并返回任何值。\r\n    assembly {\r\n        // 复制函数选择器和任何参数\r\n        calldatacopy(0, 0, calldatasize())\r\n        // 使用 facet 执行函数调用\r\n        let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)\r\n        // 获取任何返回值\r\n        returndatacopy(0, 0, returndatasize())\r\n        // 将任何返回值或错误返回给调用者\r\n        switch result\r\n            case 0 {\r\n                revert(0, returndatasize())\r\n            }\r\n            default {\r\n                return(0, returndatasize())\r\n            }\r\n    }\r\n}\r\n```\r\n\r\n首先，我们初始化 Diamond Storage。这是我们将存储的 facet 数据的地方。接下来是 `fallback()` 函数的不同之处。我们在上面的 `diamond-2` 中查看这个。在所有三个中，我们都会检查 facet 的地址是否存在。\r\n\r\n在 `diamond-1` 中我们会这样检查：\r\n\r\n```\r\n// 从函数选择器获取 facet\r\naddress facet = ds.facetAddressAndSelectorPosition[msg.sig].facetAddress;\r\nif(facet == address(0)) {\r\n    revert FunctionNotFound(msg.sig);\r\n}\r\n```\r\n\r\n在 `diamond-3` 中我们会这样检查：\r\n\r\n```\r\n// 从函数选择器获取 facet\r\naddress facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress;\r\nrequire(facet != address(0), \"Diamond: Function does not exist\");\r\n```\r\n\r\n这三种实现之间的主要区别在于，在 `diamond-2` 中，我们将选择器存储在 32 字节存储槽的映射中。\r\n\r\n最后，我们使用 Yul 来 `delegatecall()` 我们的 facets，通过复制发送到 `Diamond.sol` 的 calldata，并检查调用是否成功。\r\n\r\n这段内容标志着我们对 `Diamond.sol` 部分的讨论结束。接下来，我们将看看调用 `diamondCut()` 时发生了什么。\r\n\r\n## DiamondCut.sol\r\n\r\n如我们之前讨论的，`DiamondCut.sol` 负责在我们的 Diamond 中添加、移除和替换 facets。所有三种实现采取的方式稍有不同，但实现相同的目标。让我们看看 `diamond-1` 是如何工作的。\r\n\r\n```\r\ncontract DiamondCutFacet is IDiamondCut {\r\n    /// @notice 添加/替换/移除任意数量的函数，并可选择性地使用 delegatecall 执行函数\r\n    /// @param _diamondCut 包含 facet 地址和函数选择器\r\n    /// @param _init 要执行 _calldata 的合约或 facet 的地址\r\n    /// @param _calldata 一个函数调用，包括函数选择器和参数\r\n    ///                  _calldata 通过 delegatecall 在 _init 上执行\r\n    function diamondCut(\r\n        FacetCut[] calldata _diamondCut,\r\n        address _init,\r\n        bytes calldata _calldata\r\n    ) external override {\r\n        LibDiamond.enforceIsContractOwner();\r\n        LibDiamond.diamondCut(_diamondCut, _init, _calldata);\r\n    }\r\n}\r\n```\r\n\r\n正如你所看到的，这个合约在很大程度上依赖于 `LibDiamond.sol`。从总体概述来看，我们所做的就是检查合约的所有者是否进行了这个调用，然后调用 `diamondCut()`。让我们看看 `LibDiamond.sol` 中发生了什么。在此之前，我需要指出一个关于 `LibDiamond.sol` 的重要细节。`LibDiamond.sol` 仅使用 `internal` 函数。这可以将字节码添加到我们的合约，节省我们使用另一个 `delegatecall()` 的需要。\r\n\r\n好吧，现在我们明白了如何在 `LibDiamond.sol` 中节省一些 gas 成本，让我们看看 `diamondCut()` 的代码。\r\n\r\n```\r\n// diamondCut 的内部函数版本\r\nfunction diamondCut(\r\n    IDiamondCut.FacetCut[] memory _diamondCut,\r\n    address _init,\r\n    bytes memory _calldata\r\n) internal {\r\n    for (uint256 facetIndex; facetIndex < _diamondCut.length; facetIndex++) {\r\n        bytes4[] memory functionSelectors = _diamondCut[facetIndex].functionSelectors;\r\n        address facetAddress = _diamondCut[facetIndex].facetAddress;\r\n        if(functionSelectors.length == 0) {\r\n            revert NoSelectorsProvidedForFacetForCut(facetAddress);\r\n        }\r\n        IDiamondCut.FacetCutAction action = _diamondCut[facetIndex].action;\r\n        if (action == IDiamond.FacetCutAction.Add) {\r\n            addFunctions(facetAddress, functionSelectors);\r\n        } else if (action == IDiamond.FacetCutAction.Replace) {\r\n            replaceFunctions(facetAddress, functionSelectors);\r\n        } else if (action == IDiamond.FacetCutAction.Remove) {\r\n            removeFunctions(facetAddress, functionSelectors);\r\n        } else {\r\n            revert IncorrectFacetCutAction(uint8(action));\r\n        }\r\n    }\r\n    emit DiamondCut(_diamondCut, _init, _calldata);\r\n    initializeDiamondCut(_init, _calldata);\r\n}\r\n```\r\n\r\n我们为此函数传递了与之前相同的参数。首先，我们在 `FacetCut` 结构中循环。循环内部，我们获取我们的函数选择器和 facet 函数的地址。我们确保 facet 的选择器是有效的，不然就会回退。接下来，我们检查我们对该特定函数执行的操作（添加、替换或移除）。在找到操作后，我们调用与该操作相对应的辅助函数。最后，我们发出一个事件，为我们的用户提供透明度。最终，我们通过 `initializeDiamondCut()` 验证我们的 facet 是否正常工作，检查我们的合约是否有代码并可以通过 `delegatecall()` 使用 `_calldata` 调用。\r\n\r\n现在我们明白了 `diamondCut()` 的工作原理，让我们查看如何执行每个操作，从 `Add` 开始。\r\n\r\n```\r\nfunction addFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {\r\n    if(_facetAddress == address(0)) {\r\n        revert CannotAddSelectorsToZeroAddress(_functionSelectors);\r\n    }\r\n    DiamondStorage storage ds = diamondStorage();\r\n    uint16 selectorCount = uint16(ds.selectors.length);\r\n    enforceHasContractCode(_facetAddress, \"LibDiamondCut: Add facet has no code\");\r\n    for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {\r\n        bytes4 selector = _functionSelectors[selectorIndex];\r\n        address oldFacetAddress = ds.facetAddressAndSelectorPosition[selector].facetAddress;\r\n        if(oldFacetAddress != address(0)) {\r\n            revert CannotAddFunctionToDiamondThatAlreadyExists(selector);\r\n        }\r\n        ds.facetAddressAndSelectorPosition[selector] = FacetAddressAndSelectorPosition(_facetAddress, selectorCount);\r\n        ds.selectors.push(selector);\r\n        selectorCount++;\r\n    }\r\n}\r\n```\r\n\r\n输入参数是 facet 合约的地址和我们正在处理的特定函数的选择器。我们希望通过检查地址是否是零地址来验证这个合约是否存在。接下来，我们初始化 Diamond Storage。然后，我们获得钻石已经拥有的选择器数量。之后，我们检查我们的 facet 是否有合约代码。现在，我们需要验证这个函数是否已经存在于 Diamond 中。我们通过遍历函数选择器并检查地址是否已经存在来做到这一点。否则，我们将选择器推送到存储的选择器数组中。\r\n\r\n现在，让我们看看如何替换一个 facet！\r\n\r\n```\r\nfunction replaceFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {\r\n    DiamondStorage storage ds = diamondStorage();\r\n    if(_facetAddress == address(0)) {\r\n        revert CannotReplaceFunctionsFromFacetWithZeroAddress(_functionSelectors);\r\n    }\r\n    enforceHasContractCode(_facetAddress, \"LibDiamondCut: Replace facet has no code\");\r\n    for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {\r\n        bytes4 selector = _functionSelectors[selectorIndex];\r\n        address oldFacetAddress = ds.facetAddressAndSelectorPosition[selector].facetAddress;\r\n        // 不能替换不可变函数 -- 在这种情况下，函数直接定义在 diamond 中\r\n        if(oldFacetAddress == address(this)) {\r\n            revert CannotReplaceImmutableFunction(selector);\r\n        }\r\n        if(oldFacetAddress == _facetAddress) {\r\n            revert CannotReplaceFunctionWithTheSameFunctionFromTheSameFacet(selector);\r\n        }\r\n        if(oldFacetAddress == address(0)) {\r\n            revert CannotReplaceFunctionThatDoesNotExists(selector);\r\n        }\r\n        // 替换旧的 facet 地址\r\n        ds.facetAddressAndSelectorPosition[selector].facetAddress = _facetAddress;\r\n    }\r\n}\r\n```\r\n\r\n正如你所注意到的，`Replace` 开始的方式与 `Add` 一样。我们初始化 Diamond Storage，检查 facet 是否具有有效地址和代码大小，然后循环遍历选择器。首先，我们检查选择器是否是不可变的。接着，我们检查我们要替换的函数是否是我们添加的相同函数。之后，我们验证 facet 地址是否有效。否则，我们替换我们的 facet。\r\n\r\n现在，让我们查阅我们最后一个操作，`Remove`。\r\n\r\n```\r\nfunction removeFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {\r\n    DiamondStorage storage ds = diamondStorage();\r\n    uint256 selectorCount = ds.selectors.length;\r\n    if(_facetAddress != address(0)) {\r\n        revert RemoveFacetAddressMustBeZeroAddress(_facetAddress);\r\n    }\r\n    for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {\r\n        bytes4 selector = _functionSelectors[selectorIndex];\r\n        FacetAddressAndSelectorPosition memory oldFacetAddressAndSelectorPosition = ds.facetAddressAndSelectorPosition[selector];\r\n        if(oldFacetAddressAndSelectorPosition.facetAddress == address(0)) {\r\n            revert CannotRemoveFunctionThatDoesNotExist(selector);\r\n        }\r\n\r\n\r\n        // 不能移除不可变函数 -- 函数直接定义在 diamond 中\r\n        if(oldFacetAddressAndSelectorPosition.facetAddress == address(this)) {\r\n            revert CannotRemoveImmutableFunction(selector);\r\n        }\r\n        // 用最后一个选择器替换选择器\r\n        selectorCount--;\r\n        if (oldFacetAddressAndSelectorPosition.selectorPosition != selectorCount) {\r\n            bytes4 lastSelector = ds.selectors[selectorCount];\r\n            ds.selectors[oldFacetAddressAndSelectorPosition.selectorPosition] = lastSelector;\r\n            ds.facetAddressAndSelectorPosition[lastSelector].selectorPosition = oldFacetAddressAndSelectorPosition.selectorPosition;\r\n        }\r\n        // 删除最后一个选择器\r\n        ds.selectors.pop();\r\n        delete ds.facetAddressAndSelectorPosition[selector];\r\n    }\r\n}\r\n```\r\n\r\n同样，我们首先初始化 Diamond Storage，检查 facet 是否具有有效地址，然后遍历选择器。接下来，我们获取函数选择器和存储中的位置。我们验证它确实存在，然后确认它不是不可变的。此后，我们将选择器移动到数组的末尾，并调用 `pop()` 将其移除。\r\n\r\n`diamond-2` 和 `diamond-3` 两者都实现了与 `diamond-1` 的 `diamondCut()` 函数相同的目标，但使用了不同的语法和架构。由于本文章的简化起见，我们将不对此进行详细讲解。然而，如果有足够的人对了解它们的工作方式感兴趣，我可以在将来撰写一篇新的文章，深入讨论这些实现的区别。\r\n\r\n## LoupeFacet.sol\r\n\r\n现在我们明白了如何更新我们的 Diamond 中的函数，让我们看看我们如何查看我们的 facets。请记住，对于 `diamond-1` 和 `diamond-2`，不建议在链上调用这些函数。`diamond-3`，然而，非常优化了在链上调用这些函数。同样，我们将只讨论 `diamond-1`，但如果你理解正在发生的事情，你应该能够理解并实现 Diamond Standard 的任何其他实现。\r\n\r\n我们将首先查看 `facets()`。`facets()` 返回 Diamond 的所有 facets 及其选择器。\r\n\r\n```\r\nfunction facets() external override view returns (Facet[] memory facets_) {\r\n    LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();\r\n    uint256 selectorCount = ds.selectors.length;\r\n    // 创建一个设置为可能的最大大小的数组\r\n    facets_ = new Facet[](selectorCount);\r\n    // 创建一个数组以计算每个 facet 的选择器数量\r\n    uint16[] memory numFacetSelectors = new uint16[](selectorCount);\r\n    // facets 的总数量\r\n    uint256 numFacets;\r\n    // 循环遍历函数选择器\r\n    for (uint256 selectorIndex; selectorIndex < selectorCount; selectorIndex++) {\r\n        bytes4 selector = ds.selectors[selectorIndex];\r\n        address facetAddress_ = ds.facetAddressAndSelectorPosition[selector].facetAddress;\r\n        bool continueLoop = false;\r\n        // 查找函数选择器数组并将选择器添加到其中\r\n        for (uint256 facetIndex; facetIndex < numFacets; facetIndex++) {\r\n            if (facets_[facetIndex].facetAddress == facetAddress_) {\r\n                facets_[facetIndex].functionSelectors[numFacetSelectors[facetIndex]] = selector;\r\n                numFacetSelectors[facetIndex]++;\r\n                continueLoop = true;\r\n                break;\r\n            }\r\n        }\r\n        // 如果选择器的函数选择器数组存在，则继续循环\r\n        if (continueLoop) {\r\n            continueLoop = false;\r\n            continue;\r\n        }\r\n        // 创建一个新的函数选择器数组\r\n        facets_[numFacets].facetAddress = facetAddress_;\r\n        facets_[numFacets].functionSelectors = new bytes4[](selectorCount);\r\n        facets_[numFacets].functionSelectors[0] = selector;\r\n        numFacetSelectors[numFacets] = 1;\r\n        numFacets++;\r\n    }\r\n    for (uint256 facetIndex; facetIndex < numFacets; facetIndex++) {\r\n        uint256 numSelectors = numFacetSelectors[facetIndex];\r\n        bytes4[] memory selectors = facets_[facetIndex].functionSelectors;\r\n        // 设置选择器的数量\r\n        assembly {\r\n            mstore(selectors, numSelectors)\r\n        }\r\n    }\r\n    // 设置 facets 的数量\r\n    assembly {\r\n        mstore(facets_, numFacets)\r\n    }\r\n}\r\n```\r\n\r\n请注意，没有输入参数，我们指定将返回 `factes_`。`factes_` 是一个看起来像这样的数据结构：\r\n\r\n```\r\nstruct Facet {\r\n    address facetAddress;\r\n    bytes4[] functionSelectors;\r\n}\r\n```\r\n\r\n该函数开始时我们初始化 Diamond Storage。接下来，我们获取选择器的数量。之后，我们在函数结束时初始化一个数组以返回。然后，我们创建一个数组来跟踪每个 facet 的函数数量，并一个变量来记录 facets 的数量。接下来，我们循环遍历我们的函数选择器。在我们的循环中，我们查找我们的选择器属于哪个 facet。我们必须遍历 facets 以查找哪个地址与我们当前函数选择器的地址匹配。找到我们的 facet 后，如果它存在，就将我们的函数选择器添加到该 facet 的数组中。否则，创建数组。完成遍历后，我们再遍历一次 facets。在这个循环中，我们将数量设置为内存以方便稍后返回。最后，我们存储 facets 的数量并返回。我们最初将数组初始化为可能的最大大小。现在我们知道我们具体要返回的选择器和 facets 数量，我们告诉 Solidity 返回正确的数组大小。\r\n\r\n接下来我们要查看的函数是 `facetFunctionSelectors()`。`facetFunctionSelectors()` 返回特定 facet 的函数选择器。它以目标 facet 的地址作为参数，并返回一个 `bytes4[]` 数组，代表函数选择器。\r\n\r\n```\r\nfunction facetFunctionSelectors(address _facet) external override view returns (bytes4[] memory _facetFunctionSelectors) {\r\n    LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();\r\n    uint256 selectorCount = ds.selectors.length;\r\n    uint256 numSelectors;\r\n    _facetFunctionSelectors = new bytes4[](selectorCount);\r\n    // 循环遍历函数选择器\r\n    for (uint256 selectorIndex; selectorIndex < selectorCount; selectorIndex++) {\r\n        bytes4 selector = ds.selectors[selectorIndex];\r\n        address facetAddress_ = ds.facetAddressAndSelectorPosition[selector].facetAddress;\r\n        if (_facet == facetAddress_) {\r\n            _facetFunctionSelectors[numSelectors] = selector;\r\n            numSelectors++;\r\n        }\r\n    }\r\n    // 设置数组中的选择器数量\r\n    assembly {\r\n        mstore(_facetFunctionSelectors, numSelectors)\r\n    }\r\n}\r\n```\r\n\r\n我们再次初始化 Diamond Storage。然后，我们获取函数选择器的数量，并初始化返回数组。接下来，我们循环遍历选择器。在这里，我们检查选择器的地址是否与我们的目标 facet 相同。如果相同，我们存储该函数选择器。最后，我们存储选择器的数量并返回。\r\n\r\n现在，我们将查看 `facetAddresses()`，该函数返回我们 Diamond 的 facets 相应的地址数组。\r\n\r\n```\r\nfunction facetAddresses() external override view returns (address[] memory facetAddresses_) {\r\n    LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();\r\n    uint256 selectorCount = ds.selectors.length;\r\n    // 创建一个设置为可能的最大大小的数组\r\n    facetAddresses_ = new address[](selectorCount);\r\n    uint256 numFacets;\r\n    // 循环遍历函数选择器\r\n    for (uint256 selectorIndex; selectorIndex < selectorCount; selectorIndex++) {\r\n        bytes4 selector = ds.selectors[selectorIndex];\r\n        address facetAddress_ = ds.facetAddressAndSelectorPosition[selector].facetAddress;\r\n        bool continueLoop = false;\r\n        // 查看我们是否已经收集过该地址，如果已经收集则结束循环\r\n        for (uint256 facetIndex; facetIndex < numFacets; facetIndex++) {\r\n            if (facetAddress_ == facetAddresses_[facetIndex]) {\r\n                continueLoop = true;\r\n                break;\r\n            }\r\n        }\r\n        // 如果我们已经有了地址，则继续循环\r\n        if (continueLoop) {\r\n            continueLoop = false;\r\n            continue;\r\n        }\r\n        // 包含地址\r\n        facetAddresses_[numFacets] = facetAddress_;\r\n        numFacets++;\r\n    }\r\n    // 设置数组中的 facet 地址数量\r\n    assembly {\r\n        mstore(facetAddresses_, numFacets)\r\n    }\r\n}\r\n```\r\n\r\n首先，我们初始化 Diamond Storage，获取函数选择器的数量，并初始化返回数组。我们再次循环遍历选择器。我们检查选择器的 facet 的地址。然后，我们检查是否已经见过该地址。如果见过，我们就跳过本次循环。否则，我们将该新地址添加到返回数组中。我们再次更新数组的大小并返回。\r\n\r\n我们将要查看的 `DiamondLoupeFacet.sol` 的最后一个函数是 `facetAddress`。该函数在提供一个函数选择器时，返回相应的 facet 地址。\r\n\r\n```\r\nfunction facetAddress(bytes4 _functionSelector) external override view returns (address facetAddress_) {\r\n    LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();\r\n    facetAddress_ = ds.facetAddressAndSelectorPosition[_functionSelector].facetAddress;\r\n}\r\n```\r\n\r\n这个函数相当简单。我们初始化 Diamond Storage，并利用存储在选择器中的方式返回我们的 facet 地址。\r\n\r\n这标志着我们对 loupe facet 部分讨论的结束。我们现在知道 Diamond Standard 如何为其用户提供透明度。接下来，我们将查看部署 Diamond Standard 的工作原理。\r\n\r\n## 如何部署 Diamond Standard\r\n\r\n在部署 Diamond 时，你需要先部署 facets，然后再部署 `Diamond.sol`。这样你可以告诉 Diamond 它将调用哪些合约。部署 Diamond 的最简单方法是使用 npm 包 `diamond-util`。以下是如何安装它：\r\n\r\n```\r\nnpm i diamond-util\r\n\r\n安装完成后，你可以使用以下代码部署你的 Diamond。```\r\n// eslint-disable-next-line no-unused-vars\r\nconst deployedDiamond = await diamond.deploy({\r\n  diamondName: 'LeaseDiamond',\r\n  facets: [\\\r\n    'DiamondCutFacet',\\\r\n    'DiamondLoupeFacet',\\\r\n    'Facet1',\\\r\n    'Facet2'\\\r\n  ],\r\n  args: [/* 你的参数 */]\r\n})\r\n\r\n// 初始化 facets\r\nconst diamondCutFacet = await ethers.getContractAt('DiamondCutFacet', deployedDiamond.address)\r\nconst diamondLoupeFacet = await ethers.getContractAt('DiamondLoupeFacet', deployedDiamond.address)\r\nconst facet1 = await ethers.getContractAt('Facet1', deployedDiamond.address)\r\nconst facet2 = await ethers.getContractAt('Facet2', deployedDiamond.address)\r\n```\r\n\r\n这个库为我们处理了大部分工作！我们要做的就是列出我们的 facets，库会和 Diamond 一起部署它们。请注意，当我们使用 `ethers.js` 初始化我们的合同时，我们将地址设置为 `Diamond.sol` 的地址。\r\n\r\n如果你想添加一个新的 facet，可以通过调用 `DiamondCutFacet.sol` 来实现。下面是一个示例。\r\n\r\n```\r\nconst FacetCutAction = { Add: 0, Replace: 1, Remove: 2 }\r\n\r\nconst Facet3 = await ethers.getContractFactory('Facet3')\r\nconst facet3 = await Facet3.deploy()\r\nawait facet3.deployed()\r\nconst selectors = getSelectors(facet3).remove(['supportsInterface(bytes4)'])\r\ntx = await diamondCutFacet.diamondCut(\r\n  [{\\\r\n    facetAddress: facet3.address,\\\r\n    action: FacetCutAction.Add,\\\r\n    functionSelectors: selectors\\\r\n  }],\r\n  ethers.constants.AddressZero, '0x', { gasLimit: 800000 }\r\n)\r\n\r\nreceipt = await tx.wait()\r\n```\r\n\r\n如你所见，我们首先部署我们的 facet。然后我们使用我们的 npm 包获取函数的 selectors。接着我们调用 `DiamondCutFacet.sol` 更新我们的 Diamond。`Replace` 的工作方式类似，但你必须确保要替换的 selectors 已经在 Diamond 中。`Remove` 同样工作方式类似，但确保你传入的 selectors 是你想要移除的那些。\r\n\r\n恭喜你！你现在知道如何使用 Diamond 标准来创建和部署区块链应用程序了！\r\n\r\n## 结论\r\n\r\n这篇文章到此结束，希望我能帮助你理解 Diamond 标准的复杂性，以及如何在你自己的项目中实现它。\r\n\r\n有关 Diamond 标准的进一步阅读，请查看以下链接。\r\n\r\nEIP-2535: [https://eips.ethereum.org/EIPS/eip-2535](https://eips.ethereum.org/EIPS/eip-2535)\r\n\r\n要阅读不同 Diamond 标准实现的内容: [https://github.com/mudgen/diamond](https://github.com/mudgen/diamond)\r\n\r\n`diamond-1` 模板: [https://github.com/mudgen/diamond-1-hardhat](https://github.com/mudgen/diamond-1-hardhat)\r\n\r\n`diamond-2` 模板: [https://github.com/mudgen/diamond-2-hardhat](https://github.com/mudgen/diamond-2-hardhat)\r\n\r\n`diamond-3` 模板: [https://github.com/mudgen/diamond-3-hardhat](https://github.com/mudgen/diamond-3-hardhat)\r\n\r\nDiamond 标准的 Gas 优势: [https://learnblockchain.cn/article/12557?s=w](https://learnblockchain.cn/article/12557?s=w)\r\n\r\n要阅读 Nick Mudge 关于 Diamond 标准的博客文章: [https://eip2535diamonds.substack.com/](https://eip2535diamonds.substack.com/)\r\n\r\n观看 Nick Mudge 以视频形式讲解 Diamond 标准: [https://www.youtube.com/watch?v=9-MYz75FA8o](https://www.youtube.com/watch?v=9-MYz75FA8o)\r\n\r\n如果你编写或已经编写了符合 Diamond 标准的智能合约，并希望由 Nick Mudge 的审计公司进行审计，请查看他们的网站: [https://www.perfectabstractions.com/](https://www.perfectabstractions.com/)\r\n\r\n如果你有任何问题，或者希望我做关于其他主题的教程，请在下面留言。\r\n\r\n如果你想支持我制作教程，以下是我的以太坊地址: 0xD5FC495fC6C0FF327c1E4e3Bccc4B5987e256794。\r\n\r\n>- 原文链接： [medium.com/@MarqyMarq/ho...](https://medium.com/@MarqyMarq/how-to-implement-the-diamond-standard-69e87dae44e6)\r\n>- 登链社区 AI 助手，为大家转译优秀英文文章，如有翻译不通的地方，还请包涵～"},"author":{"user":"https://learnblockchain.cn/people/26040","address":null},"history":null,"timestamp":1741695900,"version":1}