钻石代理模式详解

文章详细介绍了ERC-2535钻石模式(Diamond Pattern),这是一种代理模式,代理合约可以同时使用多个实现合约。文章深入讨论了钻石模式的实现原理、优势、以及与透明可升级代理(Transparent Upgradeable Proxy)和UUPS的区别。还介绍了钻石模式的应用场景和最佳实践,包括如何实现不可变钻石和可升级钻石,并提供了相关代码示例。

钻石模式 (ERC-2535)

钻石模式 (ERC-2535) 是一种 代理模式,其中代理合约同时使用多个实现合约,这与 透明可升级代理UUPS 不同,后者在任何时刻仅依赖单个实现合约。代理合约根据其接收到的 calldata 的 函数选择器 来确定调用哪个实现合约进行 delegatecall(具体机制稍后描述):

钻石代理图

拥有多个实现合约的优点之一是代理合约可以使用的逻辑数量没有实际的上限。回想一下,EVM 对智能合约字节码的大小限制为 24kb。如果开发者需要部署多达 48kb 的字节码,一种可行的解决方案是使用 fallback-extension 模式。对于超过 48kb 的字节码,钻石模式是最常见的解决方案。

在钻石模式的命名法中,“代理合约”被称为钻石,而“实现合约”被称为“面”。这两个术语指代相同的东西,导致了一些困惑,所以我们要强调一点:

  • 钻石 = 代理合约
  • 面 = 实现合约

通过更改一个或多个实现合约(面),可以升级钻石(代理)。或者,钻石可以是不可升级的(不可变或 immutable),通过不支持改变面(实现合约)的机制来实现。

学习钻石模式

由于同时处理多个实现合约所产生的复杂性,钻石代理享有“专家设计模式”的声誉。事实上,钻石模式在 EVM 开发者中有些争议,因为它被认为是复杂的(我们在此不参与辩论或进行复杂性判断)。

规范本身相对较小。它需要四个公共的视图函数 — 但如果钻石是可升级的,则需要一个用于切换实现合约的第五个状态变更函数。钻石模式仅需强制一个事件(无论合约是否可升级)。尽管规范很小,实现 这四个(或五个)函数比其他代理模式复杂得多。

但是,若具备正确的先决条件(要求相对较高!),钻石模式并不特别难以构思。我们假设读者对我们 代理模式书 的前十三章所涵盖的主题很熟悉。如果你没有阅读这些章节或者对这些主题不熟悉,学习钻石模式将会十分艰难,因此确保已经具备相关的先决条件。

以下是该模式需要解决的一些问题:

  • 当钻石接收到一个交易时,它如何知道调用哪个实现合约?
  • 如果一个面(实现合约)被升级,代理合约(钻石)如何知道新面(实现合约)支持哪个函数,且可能哪些函数不再支持?
  • 升级逻辑应该放在哪里 — 在代理字节码中还是在面中?
  • 由于每个实现合约不直接知道其他实现合约,如何避免存储碰撞?
  • 外部角色如何知道钻石代理支持哪些函数?继承一个接口是不够的,因为函数在升级期间可能会发生变化。
  • 如果一个实现合约内部的函数想要调用另一个实现合约的函数,该交易如何实现?

在本文中,我们将展示如何解决上述所有问题。请注意,ERC-2535 不要求钻石代理是可升级的。代理可以具有硬编码的实现合约,但仍然是有效的钻石。

为了在开始时保持简单,我们首先展示不可变钻石。

不可变钻石

不可变钻石,也叫静态钻石单切钻石,是一个代理合约,具有多个实现合约 — 并且没有实现合约可以被升级(可升级钻石通过移除升级功能可以变为不可变)。

钻石模式作为代理合约

与 UUPS 和透明可升级代理所使用的 OpenZeppelin Proxy 相比,钻石代理的 代理 部分的代码应该是熟悉的:

// 查找被调用 функции 的面,并执行该
// 函数如果找到面并返回任何值。
fallback() external payable {

  // 从函数选择器获取面
  address facet = facetAddress(msg.sig);
  require(facet != address(0));

  // 以下代码与 OpenZeppelin Proxy.sol 相同
  // 从面执行外部函数,并返回任何值。
  assembly {
    // 复制函数选择器和任何参数
    calldatacopy(0, 0, calldatasize())
    // 使用该面执行函数调用
    let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
    // 获取返回值
    returndatacopy(0, 0, returndatasize())
    // 将返回值或错误返回给调用者
    switch result
      case 0 {revert(0, returndatasize())}
      default {return (0, returndatasize())}
  }
}

与传统代理的唯一区别是两行代码:

address facet = facetAddress(msg.sig);
require(facet != address(0));

上述代码通过 msg.sig 获取交易的前四个字节(函数选择器),然后使用 facetAddress() 来确定实现该函数的面的地址(注意 facetAddress() 可以是代理逻辑的一部分,也可以在其他一个面中存在并通过 delegatecall 调用 — 这将在后面讨论)。代理随后与其接收到的相同 calldata 一起调用该地址。

通过函数选择器获取面地址

EIP-2535 并未指定如何将函数选择器映射到实现合约的地址。对于静态钻石,一个合理的解决方案是硬编码关系。这将是我们下一节采用的方法。

对于可升级钻石,显然硬编码选择器不可行,我们必须依靠映射,如我们在相关章节将看到的那样。

基于函数选择器的条件分支

下面展示一个具有两个实现合约(面)的代理合约:

  • 第一个实现合约 Add 暴露一个公共函数 add(),返回其参数的和。
  • 第二个实现合约 Multiply 暴露两个公共函数 multiply()exponent(),其功能与名称所示相同。

下面的代码展示了一个使用两个实现合约的单个代理。在 Diamond 中的 facetAddress() 函数接受 msg.sig 并返回实现该函数签名的面的地址(如果有的话)。下面的代码尚未符合钻石标准,但我们将使其逐步符合标准。

// 第一个实现合约
contract Add {
    // 选择器: 0x771602f7
    function add(uint256 x, uint256 y) external pure returns (uint256 z) {
        z = x + y;
    }
}

// 第二个实现合约
contract Multiply {
    // 选择器: 0x165c4a16
    function multiply(uint256 x, uint256 y) external pure returns (uint256 z) {
        z = x * y;
    }

    // 选择器: 0x2f8cd8b1
    function exponent(uint256 x, uint256 y) external pure returns (uint256 z) {
        z = x ** y;
    }
}

// 代理合约
contract Diamond {
    address immutable ADD_ADDR;
    address immutable MULTIPLY_ADDR;
    constructor() {
        ADD_ADDR = address(new Add());
        MULTIPLY_ADDR = address(new Multiply());
    }

    function facetAddress(bytes4 selector) public view returns (address) {
        if (selector == 0x771602f7) return ADD_ADDR;
        else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
        else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;

        else return address(0);
    }

    fallback() external payable {
        address facet = facetAddress(msg.sig);
        require(facet != address(0), "函数不存在");
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

EIP-2535 标准并未指定调用不存在的函数时的错误消息。在我们的例子中,我们回滚时使用字符串 "函数不存在",但自定义错误会更 节省 gas

为了简化,我们的钻石在 构造函数 中部署了两个面,但在实际操作中并不是这样。实际上,面是分别部署的,代理在某种方式(例如通过构造函数参数或单独的函数)中被“告知”它们。

某些钻石将面实现为具有外部函数的库,而不是合约,但 EIP-2535 并不要求面是库或合约。

为了简单起见,我们使用一系列的 else-if 语句来将函数选择器与面地址进行匹配,但如果选项很多,这并不好节省 gas。对于静态钻石,提前对选择器进行排序,然后使用二分查找会更高效 — 但我们将在后面讨论此内容。

边缘情况 — 以太转账

当转账以太时,将不会有 calldata。在这种情况下,msg.sig 将返回 0x00000000,并且这不会映射到一个面地址。如果合约不打算接收以太,这没关系。不过,如果合约预期会接收以太,或期望会对此做出反应,那么 0x00000000 应该映射到一个具有相关逻辑的函数,或者至少是一个不会回滚的函数。要记住,攻击者可以通过发送没有 calldata 或发送 0x00000000 作为 calldata 触发此函数,因此逻辑需要优雅地处理这两种情况。

为了使我们的合约符合 EIP-2535 的要求,我们必须实现四个强制性的公共查看函数,每个函数如下所述。

四个强制的公共视图函数

1/4 facetAddress()

请注意,facetAddress() 是公共的 — EIP-2535 要求钻石代理公开一个函数,带有以下签名:

function facetAddress(bytes4 selector) external view returns (address);

在这方面我们已经符合标准。

2/4 facetAddresses()

除了 facetAddress(bytes4 selector),EIP-2535 还强制要求一个名为 facetAddresses() (复数) 的函数,该函数返回钻石使用的所有面 — 换句话说,地址列表。其签名如下:

function facetAddresses() public view returns (address[] memory addresses);

由于我们钻石的面不会改变,因此我们只需硬编码面地址的列表:

contract Diamond {
    address immutable ADD_ADDR;
    address immutable MULTIPLY_ADDR;
    constructor() {
        ADD_ADDR = address(new Add());
        MULTIPLY_ADDR = address(new Multiply());
    }

    function facetAddress(bytes4 selector) public view returns (address) {
        if (selector == 0x771602f7) return ADD_ADDR;
        else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
        else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;

        else return address(0);
    }

    // ┌────────────────────┐
    // │                    │
    // │  这段代码是新代码  │
    // │                    │
    // └────────────────────┘
    function facetAddresses() public view returns (address[2] memory addresses) {
        addresses[0] = ADD_ADDR;
        addresses[1] = MULTIPLY_ADDR;
    }
    // --------

    fallback() external payable {
        address facet = facetAddress(msg.sig);
        require(facet != address(0), "函数不存在");
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

返回的地址列表(面地址)没有强制要求顺序。

3/4 facetFunctionSelectors()

给定一个面地址作为参数,facetFunctionSelectors() 返回该面所有公共函数的选择器。其签名如下:

function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);

我们可以这样实现它:

contract Diamond {
    address immutable ADD_ADDR;
    address immutable MULTIPLY_ADDR;
    constructor() {
        ADD_ADDR = address(new Add());
        MULTIPLY_ADDR = address(new Multiply());
    }

    function facetAddress(bytes4 selector) public view returns (address) {
        if (selector == 0x771602f7) return ADD_ADDR;
        else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
        else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;

        else return address(0);
    }

    function facetAddresses() public view returns (address[] memory addresses) {
        addresses[0] = ADD_ADDR;
        addresses[1] = MULTIPLY_ADDR;
    }

    // ┌────────────────────┐
    // │                    │
    // │  这段代码是新代码  │
    // │                    │
    // └────────────────────┘
    function facetFunctionSelectors(address _facet) public view returns (bytes4[] memory) {
        if (_facet == ADD_ADDR) {
            bytes4[] memory facetFunctionSelectors_ = new bytes4[](1);
            facetFunctionSelectors_[0] = 0x771602f7;
            return facetFunctionSelectors_;
        }
        else if (_facet == MULTIPLY_ADDR) {
            bytes4[] memory facetFunctionSelectors_ = new bytes4[](2);
            facetFunctionSelectors_[0] = 0x165c4a16;
            facetFunctionSelectors_[1] = 0x2f8cd8b1;

            return facetFunctionSelectors_;
        }
        else {
            bytes4[] memory facetFunctionSelectors_ = new bytes4[](0);
            return facetFunctionSelectors_;
        }
    }
    // --------

    fallback() external payable {
        address facet = facetAddress(msg.sig);
        require(facet != address(0), "函数不存在");
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

4/4 facets()

最后,EIP-2535 强制要求一个函数 facets(),该函数不接受参数并返回一个结构体列表,每个结构体包含一个面地址和该面所有函数选择器的列表。

其签名如下:

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

function facets() external view returns (Facet[] memory facets_);

facets() 返回的信息可以通过

  • 首先调用 facetAddresses() 获取所有面地址
    • 循环遍历每个面地址,将该地址放入 Facet 结构的字段 facetAddress
    • 调用该地址的 facetFunctionSelectors() 并将函数选择器列表放入结构的 functionSelectors 字段。

另一种选择是直接在函数中硬编码答案,这在某些情况下可能更高效(对于可升级的钻石,硬编码显然不可行)。在我们的钻石中,我们将使用循环方法实现 facets(),如下所示。在本代码块的底部查看新代码:

contract Add {
    // 选择器: 0x771602f7
    function add(uint256 x, uint256 y) external pure returns (uint256 z) {
        z = x + y;
    }
}

contract Multiply {
    // 选择器: 0x165c4a16
    function multiply(uint256 x, uint256 y) external pure returns (uint256 z) {
        z = x * y;
    }

    // 选择器: 0x2f8cd8b1
    function exponent(uint256 x, uint256 y) external pure returns (uint256 z) {
        z = x ** y;
    }
}

contract Diamond {
    address immutable ADD_ADDR;
    address immutable MULTIPLY_ADDR;
    constructor() {
        ADD_ADDR = address(new Add());
        MULTIPLY_ADDR = address(new Multiply());
    }

    function facetAddress(bytes4 selector) public view returns (address) {
        if (selector == 0x771602f7) return ADD_ADDR;
        else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
        else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;

        else return address(0);
    }

    function facetAddresses() public view returns (address[] memory) {
        address[] memory addresses = new address[](2);
        addresses[0] = ADD_ADDR;
        addresses[1] = MULTIPLY_ADDR;

        return addresses;
    }

    function facetFunctionSelectors(address _facet) public view returns (bytes4[] memory) {
        if (_facet == ADD_ADDR) {
            bytes4[] memory selectors = new bytes4[](1);
            selectors[0] = 0x771602f7;
            return selectors;
        }
        else if (_facet == MULTIPLY_ADDR) {
            bytes4[] memory selectors = new bytes4[](2);
            selectors[0] = 0x165c4a16;
            selectors[1] = 0x2f8cd8b1;
            return selectors;
        }
        // 对于未知的面返回空数组
        return new bytes4[](0);
    }

    // ┌────────────────────┐
    // │                    │
    // │  这段代码是新代码  │
    // │                    │
    // └────────────────────┘
    struct Facet {
        address facetAddress;
        bytes4[] functionSelectors;
    }

    function facets() public view returns (Facet[] memory) {
        address[] memory fa = facetAddresses();
        Facet[] memory _facets = new Facet[](2);

        for (uint256 i = 0; i < fa.length; i++) {
            _facets[i].facetAddress = fa[i];
            _facets[i].functionSelectors = facetFunctionSelectors(fa[i]);
        }
        return _facets;
    }
    // --------

    fallback() external payable {
        address facet = facetAddress(msg.sig);
        require(facet != address(0), "函数不存在");
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

实施这四个公共函数后,我们现在已经实现了钻石代理的所有强制外部函数。

IDiamondLoupe

整体来看,这四个函数的定义在名为 IDiamondLoupe 的接口中。所有钻石都必须实现 IDiamondLoupe。可以记住,“loupe” 是一个小放大镜,用来查看(“查看”)钻石,而这几个函数都是视图函数:

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

    /// @notice 获取所有面地址及其四字节函数选择器。
    /// @return facets_ Facet
    function facets() external view returns (Facet[] memory facets_);

    /// @notice 获取特定面支持的所有函数选择器。
    /// @param _facet 面地址。
    /// @return facetFunctionSelectors_
    function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);

    /// @notice 获取钻石使用的所有面地址。
    /// @return facetAddresses_
    function facetAddresses() external view returns (address[] memory facetAddresses_);

    /// @notice 获取支持给定选择器的面。
    /// @dev 如果未找到面则返回 address(0)。
    /// @param _functionSelector 函数选择器。
    /// @return facetAddress_ 面地址。
    function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);
}

我们离完全合规的钻石代理还差一步:记录对钻石所做的更改。这是我们接下来要讨论的主题。

DiamondCut — 记录面选择器的更改

我们正在运行的钻石示例不可升级。实际上,钻石是可升级的,可以更改它们的面及其关联的函数选择器。

对面所做的任何更改都必须记录 — 即使是对不可升级(静态)钻石,变化(添加面和函数选择器)也发生在部署时,因此需要记录。

记录面选择器的理论是,应有两种方法来确定钻石支持的函数选择器:

  1. 使用上述公共函数,或
  2. 解析日志。

甚至像我们这样硬编码的面也必须被记录,因此必须发出事件。在我们的示例中,发射必须在部署期间发生。这些日志是标准所需的。

当我们添加一个面(或进行任何更改,例如替换或移除)时,这个动作被称为 钻石切割

钻石切割并不意味着移除面,正如“切割”可能暗示的那样。它是指以某种方式更改钻石。对面进行的任何更改都需要发出 DiamondCut 事件,在接下来的代码块中定义。(记住这种“切割”的命名法是因为在实际钻石中,当实物宝石被“切割”时,会出现新的一面 — 或称作 “面” — 在切割处)。

DiamondCut 事件定义在名为 IDiamond 的新接口中。每当添加、替换或移除某个面的一项函数时,必须发出事件。DiamondCut 事件的定义如下所示:

interface IDiamond {
    enum FacetCutAction {Add, Replace, Remove}
    // Add=0, Replace=1, Remove=2

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

    event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);
}

由于 FacetCut[] 是一个列表,因此可以一次更改多个面。

与其他代理模式(如 透明可升级代理 或 [UUPS 代理](https://learnblockchain.cn/article/9221)不同,钻石没有通过更改面地址来升级的机制。仅当所有关联的函数选择器被移除时,面的 (实现合约) 才会被移除。 当用新实现地址添加函数选择器时,面也会被隐式添加。

_init_calldata 参数的作用与 OpenZeppelin 初始化器 相同 — 我们稍后会再讨论。如果不需要初始化数据,则 _init 应为 address(0)_calldata"" 或为空字节。

让我们更新我们的钻石,以便在构造函数中发出此事件:

contract Add {
    // 选择器: 0x771602f7
    function add(uint256 x, uint256 y) external pure returns (uint256 z) {
        z = x + y;
    }
}

contract Multiply {
    // 选择器: 0x165c4a16
    function multiply(uint256 x, uint256 y) external pure returns (uint256 z) {
        z = x * y;
    }

    // 选择器: 0x2f8cd8b1
    function exponent(uint256 x, uint256 y) external pure returns (uint256 z) {
            z = x ** y;
    }
}

interface IDiamond {
    enum FacetCutAction {Add, Replace, Remove}
    // Add=0, Replace=1, Remove=2

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

    event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);
}

contract Diamond is IDiamond {
    address immutable ADD_ADDR;
    address immutable MULTIPLY_ADDR;

    constructor() {
        ADD_ADDR = address(new Add());
        MULTIPLY_ADDR = address(new Multiply());

        // ┌────────────────────┐
        // │                    │
        // │  这段代码是新代码  │
        // │                    │
        // └────────────────────┘

        // 总共有 3 个面:
        // [add, [add]]
        // [multiply, [multiply, exponent]]
        // [this, [facets, facetAddress, facetAddresses, facetFunctionSelectors]]
        FacetCut[] memory _diamondCuts = new FacetCut[](3);
        enum FacetCutAction {Add, Replace, Remove}

        _diamondCuts[0].facetAddress = ADD_ADDR;
        bytes4[] memory _addFacets = new bytes4[](1);
        _addFacets[0] = 0x771602f7;

        // 添加到 _diamondCuts
        _diamondCuts[0].action = Add;
        _diamondCuts[0].functionSelectors = _addFacets;

        _diamondCuts[1].facetAddress = MULTIPLY_ADDR;
        bytes4[] memory _mulFacets = new bytes4[](2);
        _mulFacets[0] = 0x165c4a16;
        _mulFacets[1] = 0x2f8cd8b1;

        // 添加到 _diamondCuts
        _diamondCuts[1].action = Add;
        _diamondCuts[1].functionSelectors = _mulFacets;

        // 请注意,IDiamondLoupe 接口函数也被记录。
        _diamondCuts[2].facetAddress = address(this);
        bytes4[] memory _loupeFacets = new bytes4[](4);
        _loupeFacets[0] = this.facetAddress.selector;
        _loupeFacets[1] = this.facetAddresses.selector;
        _loupeFacets[2] = this.facets.selector;
        _loupeFacets[3] = this.facetFunctionSelectors.selector;

        // 添加到 _diamondCuts
        _diamondCuts[2].action = Add;
        _diamondCuts[2].functionSelectors = _loupeFacets;

        emit DiamondCut(_diamondCuts, address(0), "");

        // --------
    }

    function facetAddress(bytes4 selector) public view returns (address) {
        if (selector == 0x771602f7) return ADD_ADDR;
        else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
        else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;

        else return address(0);
    }

    function facetAddresses() public view returns (address[] memory) {
        address[] memory addresses = new address[](2);
        addresses[0] = ADD_ADDR;
        addresses[1] = MULTIPLY_ADDR;

        return addresses;
    }

    function facetFunctionSelectors(address _facet) public view returns (bytes4[] memory) {
        if (_facet == ADD_ADDR) {
            bytes4[] memory selectors = new bytes4[](1);
            selectors[0] = 0x771602f7;
            return selectors;
        }
        else if (_facet == MULTIPLY_ADDR) {
            bytes4[] memory selectors = new bytes4[](2);
            selectors[0] = 0x165c4a16;
            selectors[1] = 0x2f8cd8b1;
            return selectors;
        }
        // 对于未知的面返回空数组
        return new bytes4[](0);
    }

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

    function facets() public view returns (Facet[] memory) {
        address[] memory fa = facetAddresses();
        Facet[] memory _facets = new Facet[](2);

        for (uint256 i = 0; i < fa.length; i++) {
            _facets[i].facetAddress = fa[i];
            _facets[i].functionSelectors = facetFunctionSelectors(fa[i]);
        }
        return _facets;
    }

    fallback() external payable {
        address facet = facetAddress(msg.sig);
        require(facet != address(0), "函数不存在");
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    receive() external payable {}
}

我们现在拥有一个完全符合 EIP-2535 的钻石代理。通常,IDiamondLoupe 函数存储在单独的面中,但为了简单起见,我们将它们放在钻石中,以保持简单。

无论 IDiamondLoupe 的函数存储在钻石本身还是另一个面中,标准都要求记录它们的地址、发生的操作(添加一个面)和函数选择器。

IDiamondLoupe 和 DiamondCut 事件之间的重复

该 EIP 的一个争议方面是,函数选择器可以通过解析过去的日志和通过调用 IDiamondLoupe 中的视图函数两种方式确定。这为完成相同的事情带来了重复的逻辑。

通过公共函数公开同样的数据的理由是这使得与区块浏览器等外部系统的集成更简单。此外,升级脚本可以原子检查函数选择器是否已经存在,然后再注册一个新的。DiamondCut 事件的意图是展示升级的历史。

在 ERC-1967 中,区块浏览器可以查询存储插槽并立即识别逻辑合约 — 区块浏览器无需解析 ERC-1967 发出的日志,这些日志包含相同的信息。

使钻石可升级,实现 diamondCut 函数

EIP-2535 标准建议添加一个如下所示的 diamondCut() 函数,以便可以添加、更改或移除面。

interface IDiamondCut is IDiamond {
    /// @notice 添加/替换/移除任意数量的函数,并可选择性地执行
    ///         通过 delegatecall 调用的函数
    /// @param _diamondCut 包含面地址和函数选择器的数组
    /// @param _init 要执行 `_calldata` 的合约或面的地址
    /// @param _calldata 一个函数调用,包括函数选择器和参数
    ///                  `_calldata` 通过 delegatecall 在 `_init` 上执行
    function diamondCut(
        FacetCut[] calldata _diamondCut,
        address _init,
        bytes calldata _calldata
    ) external;
}

标准并不 要求 升级函数为 diamondCut() 或实现上述签名。开发者可以使用自己的函数,如 changeTheFacets(),但是该函数必须根据所做的面更新类型发出 DiamondCut 事件。

面和选择器的数据结构

要存储函数选择器和面的信息,至少我们希望建立一个映射,从函数选择器到面的地址:

mapping(bytes4 => address) facetToSelectors;

这个数据结构使我们能够:

  • 根据接收到的 msg.sig 找到应该进行 delegatecall 的实现合约
  • 确定是否可以添加新的函数选择器,而不与先前存在的函数选择器发生冲突。我们必须要求 facetToSelectors[selector] == address(0)
  • 确定选择器是否可以被替换或移除。使用相同的检查,我们可以确定是否在替换或移除一个不存在的选择器(该操作应当回滚)。

实现 IDiamondLoupe 函数

作为回顾,这里是我们需要支持的 IDiamondLoupe 中的视图函数:

  1. facetAddress(bytes4 selector) 给定一个函数选择器,返回该面
  2. facetAddresses() 返回所有面地址
  3. facetFunctionSelectors(address facet) 给定一个地址,返回该面的所有函数选择器
  4. facets() 返回所有面地址及其函数选择器

以下是我们对每一个函数的实现:

  1. facetAddress(bytes4 selector) 函数可以直接用 facetToSelectors 映射的视图函数 — 或者我们可以将映射设为公共的。
  2. 要返回 facetAddresses() 的所有地址,我们必须:
    1. 显式存储所有地址在列表中
    2. 显式存储所有函数选择器在一个列表中,循环遍历函数选择器,分别调用 facetAddress(bytes4 selector),然后构建唯一地址的列表。
  3. 要通过 facetFunctionSelectors(address facet) 返回一个面的所有函数选择器,我们必须:
    1. 创建一个映射 mapping(address facet => bytes4[]),该映射存储与每个映射相连的函数选择器列表
    2. 保持一个包含所有函数选择器的数组,并循环遍历该数组,调用 facetAddress(bytes4 selector),对每个返回的面添加该地址,如果返回的 facetfacetFunctionSelectors 的参数中代表的 facet
  4. 由于 facets() 返回同样的信息,我们省略对其实现的进一步讨论。

这之间存在着一种基本的权衡。如果我们使用更多的数据结构,将使链上调用视图函数更便宜,因为它们不需要“重建”这些函数返回的数据,但在升级期间需要更新的数据结构会更多。因此我们必须选择:

  • 升级更便宜,但在链上调用视图函数会更贵。
  • 调用视图函数更便宜,但升级面需要更多的簿记(增加 gas 费用)。

在链上调用任何 IDiamondLoupe 视图函数的情况极其罕见,因为它们是为链下消费而设计的。因此,选择更少的数据结构是更可取的。

面和选择器的存储变量 — 以及避免存储碰撞

如前所述,我们需要至少一个映射来存储选择器和地址信息,例如:

mapping(bytes4 => address) facetToAddress;

在钻石中,但随后在第一个存储插槽分配给任何面时,这个映射可能与该映射碰撞。

在钻石代理中,存储碰撞比其他代理模式要复杂,因为碰撞不仅可能在相同实现合约的升级中发生,也可能在面之间发生。

钻石模式并未定义如何管理存储。一种处理碰撞的简单方法是使用存储命名空间。有关如何使用命名空间的详细说明,可以参见 EIP-7201 存储命名空间。作为回顾,在存储命名空间中,一个合约的状态变量被分组到一个结构中,而这个结构的基址存储在一个伪随机槽中,通常由一个字符串的哈希值决定。因此,每个合约都有自己的基础存储槽,使得存储碰撞的可能性极小。

EIP-7201 是从更早期的一个解决方案派生而来的,该解决方案称为“diamond storage”(钻石存储)。EIP-2535 还提出了另一种模式,称为“App Storage”(应用存储)。然而,EIP-2535 并未规定存储应如何管理,因此我们仅向读者推荐一个可行的解决方案,即使用 EIP-7201。感兴趣的读者可以直接从 EIP 中了解“diamond storage”和“app storage”的模式 — 这两者都得到了 EIP 作者的推荐。

操作钻石所需的最少存储

如果我们选择保留操作钻石所需的最少存储,则代理的命名空间应该是一个具有以下字段的结构:

  • 我们需要一个选择器列表,即 bytes4[] selectors。每当我们添加一个选择器时,我们需要在此列表中扫描,以确保我们没有添加已存在的选择器。
  • 我们至少需要一个从选择器到地址的映射。然而,将选择器映射到它们在 selectors 中的索引也是有帮助的。这样,当我们移除一个选择器时,我们可以快速查找它在数组中的索引。然后,我们将该条目与最后一个条目交换,并从列表中弹出。因此,我们不再存储 selector => address,而是存储一个结构,保存地址和选择器在数组中的位置。因此,我们的映射保存 selector => (address, index_in_selectors)

下面的代码实现了以上两点:

  • selectors 是选择器的列表
  • 结构 FacetAddressAndSelectorPosition 存储 facetAddress 以及选择器在 selectors 中的索引
struct FacetAddressAndSelectorPosition {
    address facetAddress;
    uint16 selectorPosition; // 选择器在 `selectors` 中的索引
}

struct DiamondStorage {
    mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
    bytes4[] selectors;
}

该结构可以使用 EIP-7201 中相同的模式进行访问。

管理存储的 LibDiamond

为了保持简单,存放上述信息的结构和设置存储指针到该结构的函数可以保存在一个名为 LibDiamond 的单独库中。该库提供了一个函数 diamondStorage(),返回指向该结构的指针,以及 facetAddress(bytes4 selector)。在这个库中定义 facetAddress 是可选的,仅为方便。


// ┌────────────────────┐
// │                    │
// │  存储的代码        │
// │                    │
// └────────────────────┘
library LibDiamond {
        // keccak256(abi.encode(uint256(keccak256("diamond.storage")) - 1)) & ~bytes32(uint256(0xff));
        bytes32 constant DIAMOND_STORAGE_POSITION = 0xd7ce2c87e6a71bef91a0dfa43113050aa4eae7c1a7c451ae61d9077904d7cd00;

    struct DiamondStorage {
        mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
        bytes4[] selectors;
    }

        function diamondStorage()
            internal
            pure
            returns (DiamondStorage storage ds) {

            bytes32 position = DIAMOND_STORAGE_POSITION;
            assembly {
                    // 更改存储指针的槽
                ds.slot := position
            }
        }

        function facetAddress(bytes4 _functionSelector)
            external
            override
            view
            returns (address facetAddress_) {

            LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
            facetAddress_ = ds.facetAddressAndSelectorPosition[_functionSelector].facetAddress;
        }
}

对于可升级的钻石,通常把 IDiamondLoupe 函数放在它们自己的 facet 中,而不是放在代理中。对这个合约没有命名的要求,但 DiamondLoupeFacet 是一个合理的描述。下面我们展示了 DiamondLoupeFacet 使用 LibDiamond 库实现了 IDiamondLoupe 中的一部分外部 facetAddress 函数。

import { LibDiamond } from "./libraries/LibDiamond.sol";
// ┌─────────────────────┐
// │                     │
// │  DiamondLoupeFacet  │
// │                     │
// └─────────────────────┘
contract DiamondLoupeFacet is IDiamondLoupe {

    /// @notice 获取支持给定选择器的 facet 地址。
    /// @dev 如果找不到 facet,返回 address(0)。
    /// @param _functionSelector 函数选择器。
    /// @return facetAddress_ facet 地址。
    function facetAddress(bytes4 _functionSelector)
        external
        override
        view
        returns (address facetAddress_) {

        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        facetAddress_ = ds.facetAddressAndSelectorPosition[_functionSelector].facetAddress;
    }

    // 其他未显示的函数
}

钻石标准的参考实现

Nick MudgeEIP-2535 的作者维护了三个参考实现(diamond-1、diamond-2 和 diamond-3),位于以下仓库:

<https://github.com/mudgen/diamond>

这些实现优化了我们之前讨论的权衡:如果 IDiamondLoupe 中的视图函数在链上查询便宜,则更新将非常昂贵,反之亦然。

diamond-1diamond-2 尽可能少地使用存储来跟踪 facets 和选择器,仅使用一个函数选择器列表以及从函数选择器到 facet 地址的映射。下面我们看到 diamond-1 的存储。

注意,参考实现将函数选择器的数组作为 uint256 => bytes32 选择器槽的映射进行存储,以此将 8 个函数选择器打包到一个槽中。映射在 gas 上比数组稍微高效,因为它们在查找之前不会隐式检查数组的长度。这个“数组”的长度会作为 selectorCount 单独存储。

struct DiamondStorage {
    // 函数选择器 => facet 地址及选择器在选择器数组中的位置
    mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
    bytes4[] selectors;
    mapping(bytes4 => bool) supportedInterfaces;
    // 合约的所有者
    address contractOwner;
}

另一方面,diamond-3 显式存储 facet 地址和从 facet 地址到它存储的函数选择器列表的映射:

struct FacetAddressAndPosition {
    address facetAddress;
    uint96 functionSelectorPosition; // 在 facetFunctionSelectors.functionSelectors 数组中的位置
}

struct FacetFunctionSelectors {
    bytes4[] functionSelectors;
    uint256 facetAddressPosition; // facetAddress 在 facetAddresses 数组中的位置
}

struct DiamondStorage {
    // 将函数选择器映射到 facet 地址和
    // 选择器在 facetFunctionSelectors.selectors 数组中的位置
    mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition;
    // 将 facet 地址映射到函数选择器
    mapping(address => FacetFunctionSelectors) facetFunctionSelectors;
    // facet 地址
    address[] facetAddresses;
    // 用于查询合约是否实现了一个接口。
    // 用于实现 ERC-165。
    mapping(bytes4 => bool) supportedInterfaces;
    // 合约的所有者
    address contractOwner;
}

diamond-3DiamondLoupe 实现非常简单,因为它仅仅是那些存储变量的薄包装:

contract DiamondLoupeFacet is IDiamondLoupe, IERC165 {
    // 钻石 Loupe 函数
    ////////////////////////////////////////////////////////////////////
    /// 这些函数预计将被工具频繁调用。
    //
    // struct Facet {
    //     address facetAddress;
    //     bytes4[] functionSelectors;
    // }

    /// @notice 获取所有 facets 及其选择器。
    /// @return facets_ Facet
    function facets() external override view returns (Facet[] memory facets_) {
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        uint256 numFacets = ds.facetAddresses.length;
        facets_ = new Facet[](numFacets);
        for (uint256 i; i &lt; numFacets; i++) {
            address facetAddress_ = ds.facetAddresses[i];
            facets_[i].facetAddress = facetAddress_;
            facets_[i].functionSelectors = ds.facetFunctionSelectors[facetAddress_].functionSelectors;
        }
    }

    /// @notice 获取 facet 提供的所有函数选择器。
    /// @param _facet facet 地址。
    /// @return facetFunctionSelectors_
    function facetFunctionSelectors(address _facet) external override view returns (bytes4[] memory facetFunctionSelectors_) {
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        facetFunctionSelectors_ = ds.facetFunctionSelectors[_facet].functionSelectors;
    }

    /// @notice 获取一个钻石使用的所有 facet 地址。
    /// @return facetAddresses_
    function facetAddresses() external override view returns (address[] memory facetAddresses_) {
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        facetAddresses_ = ds.facetAddresses;
    }

    /// @notice 获取支持给定选择器的 facet。
    /// @dev 如果找不到 facet,返回 address(0)。
    /// @param _functionSelector 函数选择器。
    /// @return facetAddress_ facet 地址。
    function facetAddress(bytes4 _functionSelector) external override view returns (address facetAddress_) {
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        facetAddress_ = ds.selectorToFacetAndPosition[_functionSelector].facetAddress;
    }

    // 此实现 ERC-165。
    function supportsInterface(bytes4 _interfaceId) external override view returns (bool) {
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        return ds.supportedInterfaces[_interfaceId];
    }
}

diamond-1diamond-2 的视图函数更复杂,因为它们必须循环遍历所有函数选择器以列出 facet 地址,然后仅返回唯一地址。

部署一个钻石

部署和升级一个钻石的过程与其他可升级代理模式相同:

在部署时我们:

  1. 部署 facets(实现)
  2. 部署代理,使用 facets 和选择器的列表作为构造函数的参数,并在同一事务中初始化

在升级时:

  1. 部署新 facet
  2. 调用 diamondCut() 或用户自定义函数,传入要移除的选择器列表和要添加的选择器列表。可以在单个事务中添加多个新 facets。

在部署和升级时初始化存储变量

在部署代理时,我们可能想要设置一些初始状态变量,就像我们拥有一个构造函数一样。类似地,我们可能想在升级后初始化一些存储变量,类似于在 OpenZeppelin 实现的 Transparent Upgradeable Proxy 和 UUPS 中的 upgradeToAndCall

这就是 diamondCut 中最后两个参数 _init_calldata 的用处:

function diamondCut(Facets[] facets, address _init, bytes memory _calldata)

如果 _init ≠ address(0),则 diamondCut 必须使用 _calldata 作为参数代理调用 _init。因为 diamondCut 在代理(钻石)的上下文中运行,所以被代理调用的合约(_init)可以初始化代理中的存储变量。

初始化逻辑由外部合约执行。如果我们想在单个事务中设置多个存储变量,使用一个专门的智能合约作为单个事务来完成将更容易。

下面是一个示例合约:

import {LibDiamond} from "../libraries/LibDiamond.sol";

contract DiamondInit {    

    function init() external {
            // 从存储读取 ds 结构
            // (记住,这在代理的上下文中执行)
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();

        // 写入
        ds.owner = 0x456....;
        ds.usdc = 0xa1...;
                // ...
    }
}

diamondCut 的调用将传递 DiamondInit 的地址和 init() 的 ABI 编码。

初始化可以在发生 diamondCut 事务的任何时候进行 — 它并不限于 facet 的初始部署。我们还可以在替换或删除函数选择器时原子性地代理调用外部合约。

考虑要求 DiamondInit 的魔法值

EIP-2535 并不要求对 DiamondInit 合约进行安全检查,以确保其实际为合约,但检查 _init 的地址确实具有字节码会更安全。为了进一步安全,ZkSync 的钻石实现 检查对 _init 合约的代理调用是否返回一个魔法值。

钻石标准的实现细节

使用钻石标准的最安全方式是使用上面链接的参考合约,因为这些合约已经经过审计。如果你不打算在链上调用 IDiamondLoupe 函数(通常是这种情况),则 diamond-2 将是最节省 gas 的,因为它使用了符合 ERC-2535 所需的最少存储变量。

非可升级的钻石 — 使用二分查找而不是映射

对于非可升级的钻石,我们建议硬编码函数选择器之间的关系。与其使用长长的 if-else 语句来查找 facet 地址,不如使用二分查找。

作为示例,考虑以下函数,它接受一个函数选择器作为参数并返回一个地址(代码摘自 Pendle Finance,它使用钻石代理):

Pendle Finance facetAddress\(\) 代码

该代码在函数选择器上进行二分查找,并返回实现合约(facet)的地址(持有该函数的合约)。这就是全部。

上面的地址是可变地址,在构造函数中设置,并发出如要求一样的 DiamondCut 事件:

PendleRouter 构造函数

切勿使用写入存储变量的库,除非它们使用 EIP-7201

任何写入或读取非命名空间存储的 facet 都可能遭遇存储碰撞。我们建议使用 EIP-7201 来管理所有存储。

在另一个 facet 中调用函数

当一个 facet(实现合约)中的函数运行时,它是在钻石(代理合约)的上下文中执行的。因此,要调用另一个 facet 中的公共函数,我们可以调用代理地址。

考虑我们最初的示例,其中我们有一个 facet Add 和一个函数 add(),还有一个单独的 facet Multiply。假设我们想从 Multiply facet 调用 add()。下面我们展示如何实现:Multiply facet 通过指定钻石代理的地址调用 add() 函数:

interface IAdd {
    function add(uint256 x, uint256 y) external view returns (uint256);
}

contract Multiply {

    function callAdd(uint256 x, uint256 y) external {
            uint256 sum = IAdd(address(this)).add(x, y);
            // 代码的其余部分 
    }
}

这将以两次调用的方式运行:

  1. 首先,callAdd 调用自身(在代理的上下文中)
  2. 代理将函数选择器匹配到 Add facet
  3. 代理代理调用 Add facet

有些开发人员可能会惊讶于合约以这种方式调用自身,但这一确实得到了 EVM 的允许。

你可以测试以下合约以证明这一点:

contract SelfCall {

    uint256 public x = 0;

    function setToOne() external {
        x = 1;
    }

    function selfCall() external {
        SelfCall(address(this)).setToOne();

        // 或者,
        // address(this).call(abi.encodeWithSignature("setToOne()"));
    }
} 

然而,进行自我调用是有些浪费的。为了节省 gas,一个 facet 可以“直接”代理调用另一个 facet。幕后中,调用 facet 的逻辑在代理中运行,逻辑正在对另一个 facet 进行代理调用,因此该 facet 可以“看到”代理中的存储变量。为此完成的代码不会那么优雅,因为代理调用只能是低级调用。以下是摘自 EIP-2535 的示例代码:

// 从 selector => facet 地址获取映射
DiamondStorage storage ds = diamondStorage(); // EIP-7201

// 计算选择器
bytes4 functionSelector = bytes4(keccak256("functionToCall(uint256)"));

// 获取 facet 地址
address facet = ds.selectorToFacet[functionSelector];

// 代理调用
bytes memory myFunctionCall = abi.encodeWithSelector(functionSelector, 4);
(bool success, bytes memory result) = address(facet).delegatecall(myFunctionCall);

然而,上面的代码不够优雅,需要额外的三四行代码来完成简单的调用。

第三种解决方案是创建一个只包含内部函数的 Solidity 库,然后在任何需要这些内部函数的 facet 中导入它们。这可能会导致在 facets 之间的字节码重复,但如果这些 facets 不超出 24kb 限制或者大大增加部署成本,这不应该成为主要问题。

示例钻石

我们将构建一个作为计数器合约的钻石。一个 facet 将保存计数器值的视图函数,另一个 facet 将保存递增计数器的逻辑。

为了说明钻石初始化逻辑,我们将使我们的计数器从 8 开始,而不是 0。

要开始,请从 diamond-2 参考 hardhat 仓库 分叉。我们不知道任何经过审计的 Foundry 实现。

IncrementLibrary

将读取命名空间存储的代码放入一个库中,方便 facets 导入是很有帮助的。将递增逻辑直接放入库中是一个设计选择。将递增逻辑直接放在库中,对于每个调用该库中函数的其它 facet 来说,将会增加字节码大小。然而,它也简化了递增 facet 的逻辑,因为递增 facet 不需要了解命名空间存储的结构。

请注意,库中的所有函数必须是内部的,因为 Solidity 编译器期望具有外部函数的库单独部署。

pragma solidity ^0.8.0;

library LibInc {
        // keccak256(abi.encode(uint256(keccak256("RareSkills.Facet.Increment")) - 1)) ^ bytes32(uint256(0xff))
        bytes32 constant STORAGE_LOCATION = 0xfa04c3581a2244f8cd60ed05a316a89d13b0e00f0bfbe2b8a2155985a9d65e00;

        struct IncrementStorage {
                uint256 x;
        }

        function incStorage()
                internal
                pure
                returns (IncrementStorage storage iStor) {

                bytes32 location = STORAGE_LOCATION;
        assembly {
            iStor.slot := location
        }
        }

    function x()
            internal
            view
            returns (uint256 x) {

        x = incStorage().x;
    }

    function increment() internal {
        incStorage().x++;
    }
}

添加文件 contracts/libraries/LibInc.sol

使用 DiamondInit 初始化存储变量

初始化发生在一个合约中,该合约被 diamondCut() 合约进行代理调用。参考实现中,这在 contracts/upgradeInitializers/DiamondInit.sol 中。

由于篇幅原因,我们不展示整个合约。将以下代码添加到 DiamondInit.sol 中:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 添加这一行
import {LibInc} from "../libraries/LibInc.sol";
//... 

contract DiamondInit {    

    // 你可以在这个函数中添加参数,以便传入 
    // 数据来设置你自己的状态变量
    function init() external {
        // ...

                // 添加以下行
        LibInc.IncrementStorage storage _is = LibInc.incStorage();
        _is.x = 8; // 将 x 初始化为 8
    }
}

仅查看 x 的 facet

contracts/facets/ 中创建一个新文件 LibIncFacet.sol

pragma solidity ^0.8.0;

import { LibInc } from "../libraries/LibInc.sol";

contract IncViewFacet {

   function x() external view returns (uint256 x) {
          x = LibInc.x();
   }
}

仅递增 x 的 facet

contracts/facets 中创建 IncrementFacet.sol

import { LibInc } from "../libraries/LibInc.sol";

contract IncrementFacet {
        function increment() external {
        LibInc.increment();
        }
}

添加以下测试

将以下测试添加到 test/diamondTest.js 中:

  it.only('test increment', async () => {
    const incViewInterface = await ethers.getContractAt('IncViewFacet', diamondAddress)

    const initialX = await incViewInterface.x()
    console.log(initialX.toString())
    assert.equal(initialX, 8)

    const incrementInterface = await ethers.getContractAt('IncrementFacet', diamondAddress)
    await incrementInterface.increment();

    const afterX = await incViewInterface.x()
    assert.equal(afterX, 9)
  });

更新 scripts/deploy.js 部署新 facets

打开 scripts/deploy.js 并添加 facets 的合约名称。Hardhat 足够智能,可以在没有指定文件路径的情况下找到合约。下面的更改如下图红框所示:

高亮代码变化

更新测试

diamondTest.js 文件中的 before 钩子更新为以下内容,以部署新的 facets。

简单提一下,Solidity 编译器能够自动输出公共/外部函数的函数选择器。例如,假设我们有以下存储在 C.sol 的合约:

contract C {
        function foo() public {}
        function bar() external {}
}

如果我们在合约上运行 solc --hashes C.sol,我们将得到以下输出:

======= C.sol:C =======
Function signatures:
febb0f7e: bar()
c2985578: foo()

本仓库提供的脚本使用这种技术提取函数选择器,为我们省去了显式指定函数选择器的麻烦。

同样,hardhat 能够在没有指定文件路径的情况下找到合约:

Hardhat diff 截图

运行测试:

npx hardhat test

请注意,其他测试将失败,因为它们并不期望新的选择器。我们通过在测试中使用 .only 修饰符来忽略这一点。.only 修饰符将防止运行其他单元测试,仅运行修改了 .only 的单元测试。

如果你遇到问题,请确保使用 Node 版本 20。

总结

  • 钻石模式是一个具有多个实现合约的代理。
  • 钻石知道基于传入 calldata 的函数选择器哪个 facet 进行代理调用。
  • 如果钻石是可升级的,选择器到实现地址的映射可以通过 diamondCut 进行更改。更改该映射的逻辑并不在标准中规定,它可以是代理字节码的一部分或某个 facet 的一部分。
  • 钻石中的所有 facets 和选择器必须通过发出的事件和 IDiamondLoupe 中的公共函数确定。
  • IDiamondLoupe 中查看函数的 gas 成本可以通过添加更多数据结构来降低。然而,这增加了升级成本。在几乎所有情况下,我们应选择更少的数据结构,因为 IDiamondLoupe 函数被用于链下用户。

我们要感谢 Nick Mudge 对本文章早期版本提出的意见。

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

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/