如何实施钻石标准

  • MarqyMarq
  • 发布于 2023-02-08 20:17
  • 阅读 27

本文章深入介绍了Diamond Standard(EIP-2535)的原理及实现,作者Nick Mudge概述了其优点和组件,如Diamond.sol、DiamondLoupeFacet.sol和DiamondCutFacet.sol等,强调其在提高智能合约可扩展性和透明度方面的应用,同时对Diamond Storage和App Storage的管理状态变量提出了建议。

The Diamond Standard,EIP-2535,由 Nick Mudge 创建,作为实现代理智能合约的标准化架构。在本文中,我们将讨论使用 Diamond Standard 的优点以及其工作原理。

总体概述

Diamond Standard 的运作是通过部署一个名为 Diamond.sol 的合约。Diamond.sol 随后通过 delegatecall() 调用其他智能合约。这使得 Diamond.sol 可以在 Diamond.sol 的上下文中执行被调用合约的代码。所有通过 delegatecall() 被调用的合约都称为 facets。随着时间的推移,facets 可以被替换、移除和添加,这使得开发者能够在与 EVM 兼容的区块链上创建模块化应用程序。Diamond Standard 要求你实现 DiamondLoupeFacet.solDiamondLoupeFacet.sol 负责记录你协议中的其他 facets。DiamondLoupeFacet.sol 通过允许用户查看 facet 地址和功能提供透明度。Diamond Standard 的另一个要求是 DiamondCutFacet.solDiamondCutFacet.sol 负责所有应用程序的升级。当调用时,DiamondCutFacet.sol 还会发出事件,为协议用户提供另一层透明度。虽然不一定是要求,LibDiamond.sol 是一个库,提供许多帮助函数,以便使用 Diamond Standard 编写应用程序。在使用 Diamond Standard 创建应用程序时,你可以选择两条路径来维护你的状态变量。它们被称为 App StorageDiamond Storage。在接下来的文章中,我们将详细讨论 Diamond Standard 的所有组件。

Diamond Standard 具有三种不同的实现版本。幸运的是,如果你决定选择了错误的实现,可以将应用程序升级到不同的实现。diamond-1 是最基本的实现。diamond-1 的复杂性是最容易理解的,其 gas 成本也最轻。由于 gas 成本高昂,不建议在链上调用 DiamondLoupeFacet.sol 的函数(或 diamond-2)。另一方面,diamond-3 选择优化在链上调用 DiamondLoupeFacet.sol 的函数。其权衡在于调用 DiamondCutFacet.sol 的成本更高。diamond-2diamond-1 非常相似,但在复杂性上优先考虑 gas 成本。作为开发者,你需要决定这三种实现中哪个最适合你的应用程序。

由于 Diamond Standard 的复杂性,建议在创建应用程序时遵循一般模板。我会在文章底部链接这些模板以及进一步阅读的材料。

为什么使用 Diamond Standard?

对 Diamond Standard 的主要批评是其复杂性(希望我能帮助解决这个问题)。尽管它复杂,Diamond Standard 提供了以下好处:

  • 你的应用程序在实际大小上没有限制。所有智能合约的最大大小为 24KB。由于 Diamond Standard 使用 Diamond.soldelegatecall() 它的 facets,合约的大小保持相对较小。
  • 你只需使用一个地址来实现多种功能。再次感谢 delegatecall(),你可以将所有希望的功能添加到单个智能合约中。这包括用与你的协议相同的地址实现代币。
  • 提供了一种有条理的方式来升级你的智能合约。这可以帮助审核员理解和保护你的应用程序。
  • 允许你逐步升级你的智能合约。你不必重新部署整个合约,只需添加、替换或移除一个 facet 即可为你的应用程序提供所需的功能。
  • 为你的代理合约提供了很高的透明度。正如前面提到的,DiamondLoupeFacet.sol 允许用户查看你的函数在区块链上的位置以及这些函数执行的操作。请查阅 https://louper.dev/ 以分析利用 Diamond Standard 的智能合约的功能。
  • 对于你应用程序中可以使用的 facets 的数量几乎没有限制。你无法添加更多 facets 到你的协议的唯一原因会是 Diamond.sol 没有足够的存储空间存放 facet 数据。这需要相当多的 facets,实现几乎不可能。
  • facets 可以被多个 Diamond.sol 合约重用。这样可以在部署借用功能的应用程序时节省 gas 成本。
  • 如果你愿意,可以在更高的运行设置下运行优化器。Solidity 优化器有助于优化调用外部函数时的 gas 成本。但这样会增加合约部署字节码。这可能会导致你的智能合约超出最大合约大小限制。由于你可以部署任意数量的 facets,因此可以将优化器设置为你希望的尽可能高的运行次数,而无需担心合约太大。

有许多原因你可能希望在应用程序中使用 Diamond Standard,但请记住不要使你的应用程序过于复杂。如果你的协议可以使用单个智能合约,并且上述优点不适用于你的应用程序,则没有必要使用 Diamond Standard。它只会带来更多的复杂性和 gas 成本。但是,如果你面临需要为你的应用程序提供代理模式的情况,我个人推荐使用 Diamond Standard。

App Storage 和 Diamond Storage

状态变量的架构是你的区块链应用程序最重要的方面之一。我决定首先介绍这一部分,以便让你充分了解 Diamond Standard 如何管理状态变量。显然,代理在 delegatecall() 上依赖很大,以便在主合约(Diamond.sol)的上下文中执行来自你的 facet 合约的代码。由于我们的状态变量存储全部保留在 Diamond.sol 中,我们需要确保我们的变量不会相互覆盖。在查看我们组织状态变量的正确方法之前,让我们先看一个错误管理状态变量的例子。

pragma solidity^0.8.17;

contract Main {
    uint256 public verySpecialVar;
    uint256 public notSoSpecialVar;
    // delegate calls SpecialVarManager 来更新 verySpecialVar
    function setVerySpecialVar(address _specialVarManager) external {
        _specialVarManager.delegatecall(
            abi.encodeWithSignature("writeSpecialVar()")
        );
    }
    // delegate calls NotSpecialVarManager 来更新 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。所以当我们通过 delegatecall() 调用 writeNotSpecialVar() 时,我们告诉 Main 将存储槽 0 更新为 50。Solidity 并不关心你命名变量的方式;它只关注存储槽的位置。

考虑到这一点,我们需要一种方式来组织我们的状态变量,以免覆盖存储槽。第一种方法是 Diamond Storage。

Diamond Storage 利用 Solidity 智能合约中有多少存储槽(2²⁵⁶)。Diamond Storage 的理论是,由于存储槽数量庞大,如果我们哈希一个唯一值,我们将得到一个几乎肯定不会与其他存储槽冲突的随机存储槽。这听起来可能风险很大,但实际上这与 Solidity 用于存储映射和动态数组的方式是一样的。Diamond Storage 为你的 facets 提供了保持特定于其合约的状态变量的机会,同时也允许 facets 共享状态变量(如果需要)。

由于 Diamond Standard 的复杂性需要一些设置,对于这些存储示例,我不会使用 Diamond Standard。这一部分的主要目的是理解你的状态变量如何存储。在 Diamond Standard 中的实现大致相同。

pragma solidity^0.8.17;

library SharedLib {
    // 结构体带有状态变量
    struct DiamondStorage {
        uint256 sharedVar;
    }
    // 返回带有状态变量的存储变量
    function diamondStorage() internal pure returns(DiamondStorage storage ds) {
        // 通过哈希字符串获取“随机”存储位置
        bytes32 storagePosition = keccak256(abi.encode("Diamond.Storage.SharedLib"));
        // 将我们的结构体存储槽分配到存储位置
        assembly {
            ds.slot := storagePosition
        }

    }
}
// 不是实际的 Diamond Standard
contract PseudoDiamond {
    // delegate calls Facet1 来更新 sharedVar
    function writeToSharedVar(address _facet1, uint256 _value) external {
        // 通过 delegatecall 写入
        _facet1.delegatecall(
            abi.encodeWithSignature("writeShared(uint256)", _value)
        );
    }
    // delegate calls Facet2 来读取 sharedVar
    function readSharedVar(address _facet2) external returns (uint256) {
        // 返回 delegate call 的结果
        (bool success, bytes memory _valueBytes) = _facet2.delegatecall(
            abi.encodeWithSignature("readShared()")
        );
        // 由于返回值是字节数组,我们使用汇编来检索我们的 uint
        bytes32 _value;
        assembly {
            let location := _valueBytes
            _value := mload( add(location, 0x20) )
        }
        return uint256(_value);
    }
}
contract Facet1 {
    function writeShared(uint256 _value) external {
        // 通过调用库函数初始化存储结构
        SharedLib.DiamondStorage storage ds = SharedLib.diamondStorage();
        // 写入共享变量
        ds.sharedVar = _value;

    }
}
contract Facet2 {

    function readShared() external view returns (uint256) {
        // 通过调用库函数初始化存储结构
        SharedLib.DiamondStorage storage ds = SharedLib.diamondStorage();

        // 返回共享变量
        return ds.sharedVar;

    }
}

如果你调用 PseudoDiamond.writeSharedVar(),然后 PseudoDiamond.readSharedVar(),你将看到你的值。通过使用库并索引一个“随机”的存储槽,我们可以在两个智能合约之间共享变量。当我们调用 delegatecall() 两个 facets 时,它正在查看 DiamondStorage 结构的存储位置以访问该变量。通过之间关于我们想在哪里存储数据的明确沟通,我们防止了状态变量的冲突。如果你想为仅一个 facet 拥有状态变量,你可以简单地创建一个库,类似于 SharedLib,并仅将其实现到特定的 facet 中。

App Storage 的工作方式稍微不同。你创建一个 Solidity 文件,在该文件中创建一个名为 AppStorage 的结构体。然后你可以在该结构体中放置任意多的状态变量,包括其他结构体。然后在你的智能合约中,你执行的第一件事情就是初始化 AppStorage。这就是将存储槽 0 设置到结构体开始的位置。这在合约之间创建了一个人类可读的共享状态。让我们看一个示例!

pragma solidity^0.8.17;

// 结构体带有状态变量
struct StateVars {
    uint256 sharedVar;
}
// 我们的应用存储结构体
struct AppStorage {
    StateVars state;
}
// 不是实际的 Diamond Standard
contract PseudoDiamond {
    AppStorage s;
    // delegate calls Facet1 来更新 sharedVar
    function writeToSharedVar(address _facet1, uint256 _value) external {
        // 通过 delegate call 写入
        _facet1.delegatecall(
            abi.encodeWithSignature("writeShared(uint256)", _value)
        );
    }
    // delegate calls Facet2 来读取 sharedVar
    function readSharedVar(address _facet2) external returns (uint256) {
        // 返回 delegate call 的结果
        (bool success, bytes memory _valueBytes) = _facet2.delegatecall(
            abi.encodeWithSignature("readShared()")
        );
        // 由于返回值是字节数组,我们使用汇编来检索我们的 uint
        bytes32 _value;
        assembly {
            let location := _valueBytes
            _value := mload( add(location, 0x20) )
        }
        return uint256(_value);
    }
}
contract Facet1 {
    AppStorage s;
    function writeShared(uint256 _value) external {
        // 写入我们的存储中的状态变量
        s.state.sharedVar = _value;
    }
}
contract Facet2 {
    AppStorage s;

    function readShared() external view returns (uint256) {
        // 从应用存储返回状态变量
        return s.state.sharedVar;
    }
}

如果你查看 PseudoDiamond 的存储槽 0 中的值,你会看到 100,这是我们的值!关于 App Storage 的一个重要说明是,如果你需要在部署后更新你的 AppStorage,请确保在 AppStorage 的末尾添加新的状态变量,以防止存储冲突。我个人更喜欢 App Storage 而不是 Diamond Storage,因为组织性更好,但两者都能完成工作。也值得注意的是,Diamond Storage 和 App Storage 不是互斥的。即使你使用 App Storage 来管理你的状态变量,LibDiamond.sol 也使用 Diamond Storage 来管理 facet 数据。

Diamond.sol

正如前面提到的,Diamond.sol 是在与你的应用程序交互时被调用的智能合约。如果要这样理解它,将 Diamond.sol 视为“管理”你整个应用程序的合约。所有 facet 函数将显得是 Diamond.sol 自身的函数。

让我们首先查看 diamond-1 的构造函数中发生了什么。

// 这在 diamond 构造函数中使用
// 更多参数被添加到这个结构体中
// 这避免堆栈太深的错误
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);
        // 可以在这里添加代码以执行操作和设置状态变量。
    }
    // 代码的其余部分
}

首先,在合约外部,我们有一个格式化我们数据的结构体。接下来,我们将该结构体作为参数与看起来像这样的另一个结构体一起传递:

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

FacetCut 提供了 facet 的地址、我们想要在 facet 上执行的操作(添加、替换或移除)以及该 facet 的函数的选择器。接下来,我们设置合约所有者,并添加提交给 Diamond 的任何 facet 数据。我们稍后会更详细地讨论这段代码如何工作。之后,如果我们愿意,我们可以初始化状态变量。请记住,你永远不要在 facet 的构造函数中执行任何状态变量的赋值,因为它将在 facet 智能合约内部而不是 Diamond.sol 执行。

到目前为止,这个看起来相当直接,因此让我们看看 diamond-2diamond-3 的构造函数中发生的事情。

contract Diamond {

  constructor(address _contractOwner, address _diamondCutFacet) payable {
        LibDiamond.setContractOwner(_contractOwner);
        // 添加 diamondCutFacet 中的 diamondCut 外部函数
        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), "");
    }
    // 代码的其余部分
}

这次我们只传入合约的所有者和 DiamondCutFacet.sol 的地址。接下来,我们为 Diamond 指定所有者。在此之后,我们添加 diamondCut() 函数,以便我们可以添加更多的 facets。通过首先初始化一个结构体类型为 FacetCut 的长度为 1 的内存数组来实现。然后,我们初始化另一个元素为 1 的数组。这次这个数组是 bytes4,用来存储 diamondCut() 的函数选择器。接下来,我们通过调用 Diamond Cut 接口的 diamondCut 函数并获取其选择器来将选择器赋值给数组。最终,我们可以分配 cut[0]。我们使用 DiamondCutFacet.sol 的地址,添加(因为我们想将这个 facet 添加到我们的 Diamond 中)以及选择器数组。最后,我们实际上添加了 diamondCut。这在不知道底层发生了什么的情况下显得相当多,因此如果你觉得有帮助,可以在我们讨论 DiamondCut.sol 后重新阅读这一部分。目前,仅需了解我们在构造函数中添加了 facets。

Diamond.sol 的最后部分是 fallback() 函数。这是我们如何使用 Diamond Standard 调用我们的 facets 的方式。它在所有三种实现中几乎相同,所以让我们来看看!

// 查找被调用的函数的 facet,并执行该函数
// 如果找到了 facet,则执行函数并返回任何值。
fallback() external payable {
    LibDiamond.DiamondStorage storage ds;
    bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION;
    // 获取 diamond storage
    assembly {
        ds.slot := position
    }
    // 从函数选择器获取 facet
    address facet = address(bytes20(ds.facets[msg.sig]));
    require(facet != address(0), "Diamond: Function does not exist");
    // 使用 delegatecall 从 facet 执行外部函数并返回任何值。
    assembly {
        // 复制函数选择器和任何参数
        calldatacopy(0, 0, calldatasize())
        // 使用 facet 执行函数调用
        let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
        // 获取任何返回值
        returndatacopy(0, 0, returndatasize())
        // 将任何返回值或错误返回给调用者
        switch result
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
    }
}

首先,我们初始化 Diamond Storage。这是我们将存储的 facet 数据的地方。接下来是 fallback() 函数的不同之处。我们在上面的 diamond-2 中查看这个。在所有三个中,我们都会检查 facet 的地址是否存在。

diamond-1 中我们会这样检查:

// 从函数选择器获取 facet
address facet = ds.facetAddressAndSelectorPosition[msg.sig].facetAddress;
if(facet == address(0)) {
    revert FunctionNotFound(msg.sig);
}

diamond-3 中我们会这样检查:

// 从函数选择器获取 facet
address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress;
require(facet != address(0), "Diamond: Function does not exist");

这三种实现之间的主要区别在于,在 diamond-2 中,我们将选择器存储在 32 字节存储槽的映射中。

最后,我们使用 Yul 来 delegatecall() 我们的 facets,通过复制发送到 Diamond.sol 的 calldata,并检查调用是否成功。

这段内容标志着我们对 Diamond.sol 部分的讨论结束。接下来,我们将看看调用 diamondCut() 时发生了什么。

DiamondCut.sol

如我们之前讨论的,DiamondCut.sol 负责在我们的 Diamond 中添加、移除和替换 facets。所有三种实现采取的方式稍有不同,但实现相同的目标。让我们看看 diamond-1 是如何工作的。

contract DiamondCutFacet is IDiamondCut {
    /// @notice 添加/替换/移除任意数量的函数,并可选择性地使用 delegatecall 执行函数
    /// @param _diamondCut 包含 facet 地址和函数选择器
    /// @param _init 要执行 _calldata 的合约或 facet 的地址
    /// @param _calldata 一个函数调用,包括函数选择器和参数
    ///                  _calldata 通过 delegatecall 在 _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 中节省一些 gas 成本,让我们看看 diamondCut() 的代码。

// 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 结构中循环。循环内部,我们获取我们的函数选择器和 facet 函数的地址。我们确保 facet 的选择器是有效的,不然就会回退。接下来,我们检查我们对该特定函数执行的操作(添加、替换或移除)。在找到操作后,我们调用与该操作相对应的辅助函数。最后,我们发出一个事件,为我们的用户提供透明度。最终,我们通过 initializeDiamondCut() 验证我们的 facet 是否正常工作,检查我们的合约是否有代码并可以通过 delegatecall() 使用 _calldata 调用。

现在我们明白了 diamondCut() 的工作原理,让我们查看如何执行每个操作,从 Add 开始。

function 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++;
    }
}

输入参数是 facet 合约的地址和我们正在处理的特定函数的选择器。我们希望通过检查地址是否是零地址来验证这个合约是否存在。接下来,我们初始化 Diamond Storage。然后,我们获得钻石已经拥有的选择器数量。之后,我们检查我们的 facet 是否有合约代码。现在,我们需要验证这个函数是否已经存在于 Diamond 中。我们通过遍历函数选择器并检查地址是否已经存在来做到这一点。否则,我们将选择器推送到存储的选择器数组中。

现在,让我们看看如何替换一个 facet!

function 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;
        // 不能替换不可变函数 -- 在这种情况下,函数直接定义在 diamond 中
        if(oldFacetAddress == address(this)) {
            revert CannotReplaceImmutableFunction(selector);
        }
        if(oldFacetAddress == _facetAddress) {
            revert CannotReplaceFunctionWithTheSameFunctionFromTheSameFacet(selector);
        }
        if(oldFacetAddress == address(0)) {
            revert CannotReplaceFunctionThatDoesNotExists(selector);
        }
        // 替换旧的 facet 地址
        ds.facetAddressAndSelectorPosition[selector].facetAddress = _facetAddress;
    }
}

正如你所注意到的,Replace 开始的方式与 Add 一样。我们初始化 Diamond Storage,检查 facet 是否具有有效地址和代码大小,然后循环遍历选择器。首先,我们检查选择器是否是不可变的。接着,我们检查我们要替换的函数是否是我们添加的相同函数。之后,我们验证 facet 地址是否有效。否则,我们替换我们的 facet。

现在,让我们查阅我们最后一个操作,Remove

function 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);
        }

        // 不能移除不可变函数 -- 函数直接定义在 diamond 中
        if(oldFacetAddressAndSelectorPosition.facetAddress == address(this)) {
            revert CannotRemoveImmutableFunction(selector);
        }
        // 用最后一个选择器替换选择器
        selectorCount--;
        if (oldFacetAddressAndSelectorPosition.selectorPosition != selectorCount) {
            bytes4 lastSelector = ds.selectors[selectorCount];
            ds.selectors[oldFacetAddressAndSelectorPosition.selectorPosition] = lastSelector;
            ds.facetAddressAndSelectorPosition[lastSelector].selectorPosition = oldFacetAddressAndSelectorPosition.selectorPosition;
        }
        // 删除最后一个选择器
        ds.selectors.pop();
        delete ds.facetAddressAndSelectorPosition[selector];
    }
}

同样,我们首先初始化 Diamond Storage,检查 facet 是否具有有效地址,然后遍历选择器。接下来,我们获取函数选择器和存储中的位置。我们验证它确实存在,然后确认它不是不可变的。此后,我们将选择器移动到数组的末尾,并调用 pop() 将其移除。

diamond-2diamond-3 两者都实现了与 diamond-1diamondCut() 函数相同的目标,但使用了不同的语法和架构。由于本文章的简化起见,我们将不对此进行详细讲解。然而,如果有足够的人对了解它们的工作方式感兴趣,我可以在将来撰写一篇新的文章,深入讨论这些实现的区别。

LoupeFacet.sol

现在我们明白了如何更新我们的 Diamond 中的函数,让我们看看我们如何查看我们的 facets。请记住,对于 diamond-1diamond-2,不建议在链上调用这些函数。diamond-3,然而,非常优化了在链上调用这些函数。同样,我们将只讨论 diamond-1,但如果你理解正在发生的事情,你应该能够理解并实现 Diamond Standard 的任何其他实现。

我们将首先查看 facets()facets() 返回 Diamond 的所有 facets 及其选择器。

function facets() external override view returns (Facet[] memory facets_) {
    LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
    uint256 selectorCount = ds.selectors.length;
    // 创建一个设置为可能的最大大小的数组
    facets_ = new Facet[](selectorCount);
    // 创建一个数组以计算每个 facet 的选择器数量
    uint16[] memory numFacetSelectors = new uint16[](selectorCount);
    // facets 的总数量
    uint256 numFacets;
    // 循环遍历函数选择器
    for (uint256 selectorIndex; selectorIndex < selectorCount; selectorIndex++) {
        bytes4 selector = ds.selectors[selectorIndex];
        address facetAddress_ = ds.facetAddressAndSelectorPosition[selector].facetAddress;
        bool continueLoop = false;
        // 查找函数选择器数组并将选择器添加到其中
        for (uint256 facetIndex; facetIndex < numFacets; facetIndex++) {
            if (facets_[facetIndex].facetAddress == facetAddress_) {
                facets_[facetIndex].functionSelectors[numFacetSelectors[facetIndex]] = selector;
                numFacetSelectors[facetIndex]++;
                continueLoop = true;
                break;
            }
        }
        // 如果选择器的函数选择器数组存在,则继续循环
        if (continueLoop) {
            continueLoop = false;
            continue;
        }
        // 创建一个新的函数选择器数组
        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;
        // 设置选择器的数量
        assembly {
            mstore(selectors, numSelectors)
        }
    }
    // 设置 facets 的数量
    assembly {
        mstore(facets_, numFacets)
    }
}

请注意,没有输入参数,我们指定将返回 factes_factes_ 是一个看起来像这样的数据结构:

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

该函数开始时我们初始化 Diamond Storage。接下来,我们获取选择器的数量。之后,我们在函数结束时初始化一个数组以返回。然后,我们创建一个数组来跟踪每个 facet 的函数数量,并一个变量来记录 facets 的数量。接下来,我们循环遍历我们的函数选择器。在我们的循环中,我们查找我们的选择器属于哪个 facet。我们必须遍历 facets 以查找哪个地址与我们当前函数选择器的地址匹配。找到我们的 facet 后,如果它存在,就将我们的函数选择器添加到该 facet 的数组中。否则,创建数组。完成遍历后,我们再遍历一次 facets。在这个循环中,我们将数量设置为内存以方便稍后返回。最后,我们存储 facets 的数量并返回。我们最初将数组初始化为可能的最大大小。现在我们知道我们具体要返回的选择器和 facets 数量,我们告诉 Solidity 返回正确的数组大小。

接下来我们要查看的函数是 facetFunctionSelectors()facetFunctionSelectors() 返回特定 facet 的函数选择器。它以目标 facet 的地址作为参数,并返回一个 bytes4[] 数组,代表函数选择器。

function 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);
    // 循环遍历函数选择器
    for (uint256 selectorIndex; selectorIndex < selectorCount; selectorIndex++) {
        bytes4 selector = ds.selectors[selectorIndex];
        address facetAddress_ = ds.facetAddressAndSelectorPosition[selector].facetAddress;
        if (_facet == facetAddress_) {
            _facetFunctionSelectors[numSelectors] = selector;
            numSelectors++;
        }
    }
    // 设置数组中的选择器数量
    assembly {
        mstore(_facetFunctionSelectors, numSelectors)
    }
}

我们再次初始化 Diamond Storage。然后,我们获取函数选择器的数量,并初始化返回数组。接下来,我们循环遍历选择器。在这里,我们检查选择器的地址是否与我们的目标 facet 相同。如果相同,我们存储该函数选择器。最后,我们存储选择器的数量并返回。

现在,我们将查看 facetAddresses(),该函数返回我们 Diamond 的 facets 相应的地址数组。

function facetAddresses() external override view returns (address[] memory facetAddresses_) {
    LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
    uint256 selectorCount = ds.selectors.length;
    // 创建一个设置为可能的最大大小的数组
    facetAddresses_ = new address[](selectorCount);
    uint256 numFacets;
    // 循环遍历函数选择器
    for (uint256 selectorIndex; selectorIndex < selectorCount; selectorIndex++) {
        bytes4 selector = ds.selectors[selectorIndex];
        address facetAddress_ = ds.facetAddressAndSelectorPosition[selector].facetAddress;
        bool continueLoop = false;
        // 查看我们是否已经收集过该地址,如果已经收集则结束循环
        for (uint256 facetIndex; facetIndex < numFacets; facetIndex++) {
            if (facetAddress_ == facetAddresses_[facetIndex]) {
                continueLoop = true;
                break;
            }
        }
        // 如果我们已经有了地址,则继续循环
        if (continueLoop) {
            continueLoop = false;
            continue;
        }
        // 包含地址
        facetAddresses_[numFacets] = facetAddress_;
        numFacets++;
    }
    // 设置数组中的 facet 地址数量
    assembly {
        mstore(facetAddresses_, numFacets)
    }
}

首先,我们初始化 Diamond Storage,获取函数选择器的数量,并初始化返回数组。我们再次循环遍历选择器。我们检查选择器的 facet 的地址。然后,我们检查是否已经见过该地址。如果见过,我们就跳过本次循环。否则,我们将该新地址添加到返回数组中。我们再次更新数组的大小并返回。

我们将要查看的 DiamondLoupeFacet.sol 的最后一个函数是 facetAddress。该函数在提供一个函数选择器时,返回相应的 facet 地址。

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

这个函数相当简单。我们初始化 Diamond Storage,并利用存储在选择器中的方式返回我们的 facet 地址。

这标志着我们对 loupe facet 部分讨论的结束。我们现在知道 Diamond Standard 如何为其用户提供透明度。接下来,我们将查看部署 Diamond Standard 的工作原理。

如何部署 Diamond Standard

在部署 Diamond 时,你需要先部署 facets,然后再部署 Diamond.sol。这样你可以告诉 Diamond 它将调用哪些合约。部署 Diamond 的最简单方法是使用 npm 包 diamond-util。以下是如何安装它:

npm i diamond-util

安装完成后,你可以使用以下代码部署你的 Diamond。```
// eslint-disable-next-line no-unused-vars
const deployedDiamond = await diamond.deploy({
  diamondName: 'LeaseDiamond',
  facets: [\
    'DiamondCutFacet',\
    'DiamondLoupeFacet',\
    'Facet1',\
    'Facet2'\
  ],
  args: [/* 你的参数 */]
})

// 初始化 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)

这个库为我们处理了大部分工作!我们要做的就是列出我们的 facets,库会和 Diamond 一起部署它们。请注意,当我们使用 ethers.js 初始化我们的合同时,我们将地址设置为 Diamond.sol 的地址。

如果你想添加一个新的 facet,可以通过调用 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()

如你所见,我们首先部署我们的 facet。然后我们使用我们的 npm 包获取函数的 selectors。接着我们调用 DiamondCutFacet.sol 更新我们的 Diamond。Replace 的工作方式类似,但你必须确保要替换的 selectors 已经在 Diamond 中。Remove 同样工作方式类似,但确保你传入的 selectors 是你想要移除的那些。

恭喜你!你现在知道如何使用 Diamond 标准来创建和部署区块链应用程序了!

结论

这篇文章到此结束,希望我能帮助你理解 Diamond 标准的复杂性,以及如何在你自己的项目中实现它。

有关 Diamond 标准的进一步阅读,请查看以下链接。

EIP-2535: https://eips.ethereum.org/EIPS/eip-2535

要阅读不同 Diamond 标准实现的内容: https://github.com/mudgen/diamond

diamond-1 模板: https://github.com/mudgen/diamond-1-hardhat

diamond-2 模板: https://github.com/mudgen/diamond-2-hardhat

diamond-3 模板: https://github.com/mudgen/diamond-3-hardhat

Diamond 标准的 Gas 优势: https://learnblockchain.cn/article/12557?s=w

要阅读 Nick Mudge 关于 Diamond 标准的博客文章: https://eip2535diamonds.substack.com/

观看 Nick Mudge 以视频形式讲解 Diamond 标准: https://www.youtube.com/watch?v=9-MYz75FA8o

如果你编写或已经编写了符合 Diamond 标准的智能合约,并希望由 Nick Mudge 的审计公司进行审计,请查看他们的网站: https://www.perfectabstractions.com/

如果你有任何问题,或者希望我做关于其他主题的教程,请在下面留言。

如果你想支持我制作教程,以下是我的以太坊地址: 0xD5FC495fC6C0FF327c1E4e3Bccc4B5987e256794。

  • 原文链接: medium.com/@MarqyMarq/ho...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
MarqyMarq
MarqyMarq
江湖只有他的大名,没有他的介绍。