钻石模式(ERC-2535)是一种代理模式……与一次只依赖一个实现合约的 Transparent Upgradeable Proxy 和 UUPS 不同,代理合约同时使用多个实现合约。代理合约根据接收到的 calldata 的 function selector 决定向哪个实现合约发起 delegatecall(具体机制将在后文详细说明):

拥有多个实现合约的优势之一是,代理合约可以使用的逻辑数量实际上没有上限。回顾一下,EVM 将智能合约的字节码大小限制为 24kb。如果开发者需要部署高达 48kb 的字节码,一个可行的解决方案是使用 fallback-extension pattern。对于超过 48kb 的情况,钻石模式是最常见的解决方案。
在钻石模式的术语中,“代理合约”(proxy contract)被称为 diamond(钻石),而“实现合约”(implementation contracts)被称为 facet(切面)。有两个术语指向同一个事物容易引起混淆,因此我们现在要明确这一点:
- diamond = 代理合约 (proxy contract)
- facet = 实现合约 (implementation contract)
可以通过更改一个或多个实现合约(facets)来升级 diamond(代理)。或者,如果不支持更改 facets(实现合约)的机制,diamond 也可以是不可升级的(不可变的)。
学习钻石模式
由于同时处理多个实现合约所带来的复杂性,钻石代理素有“专家级设计模式”之称。事实上,由于其所谓的复杂性,钻石模式在 EVM 开发者中存在一些争议(本文不参与争论,也不对其复杂性进行评判)。
该规范本身非常简短。它需要四个 public view 函数——但如果 diamond 是可升级的,则需要第五个改变状态的函数来切换实现合约。钻石模式只强制要求一个事件(无论合约是否可升级)。尽管规范很小,但实现这四个(或五个)函数比其他代理模式要复杂得多。
然而,如果具备合适的前置知识(这些知识相当多!),钻石模式在概念上并不难理解。我们假设读者熟悉我们的 Proxy Patterns Book 前十三章所涵盖的主题。如果你还没有阅读这些章节或还不熟悉这些主题,学习钻石模式将会非常困难,因此请确保已经掌握了前置知识。
以下是该模式需要处理的一些问题:
- 当 diamond 接收到交易时,它如何知道该调用哪个实现合约?
- 如果一个 facet(实现合约)被升级,代理合约(diamond)如何知道新 facet 支持哪些函数,以及潜在地,哪些函数不再被支持?
- 升级逻辑应该放在哪里——是在代理字节码中还是在一个 facet 中?
- 既然每个实现合约都不直接了解其他实现,如何避免存储冲突(storage collisions)?
- 外部参与者如何知道钻石代理支持哪些函数?继承接口是不够的,因为函数在升级过程中可能会发生变化。
- 如果一个实现合约内部的函数想要调用另一个实现合约中的函数该怎么办——这种交易将如何进行?
在本文中,我们将展示如何解决上述所有问题。请注意,ERC-2535 并不要求钻石代理必须是可升级的。代理可以硬编码实现合约,且仍然是一个有效的 diamond。
为了在开始时保持简单,我们将首先展示不可变的 diamond。
不可变钻石 (Immutable diamond)
不可变钻石(immutable diamond),也称为静态钻石(static diamond)或单切面钻石(single cut diamond),是一个具有多个实现合约的代理合约——并且所有实现合约都不能被升级。(可升级钻石可以通过移除升级功能变为不可变的,但我们将在稍后讨论可升级钻石)。
作为代理合约的钻石模式
如果与 UUPS 和 Transparent Upgradeable Proxy 使用的 OpenZeppelin Proxy 相比,钻石代理中代理部分的代码应该让人感到熟悉:
// Find facet for function that is called and execute the
// function if a facet is found and return any value.
fallback() external payable {
// get facet from function selector
address facet = facetAddress(msg.sig);
require(facet != address(0));
// The code below is the same as OpenZeppelin Proxy.sol
// Execute external function from facet using delegatecall and return any value.
assembly {
// copy function selector and any arguments
calldatacopy(0, 0, calldatasize())
// execute function call using the facet
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
// get any return value
returndatacopy(0, 0, returndatasize())
// return any return value or error back to the caller
switch result
case 0 {revert(0, returndatasize())}
default {return (0, returndatasize())}
}
}
上述代码与传统代理的唯一实质性区别在于这两行:
address facet = facetAddress(msg.sig);
require(facet != address(0));
上述代码使用 msg.sig 获取交易的前四个字节(即 function selector),然后使用 facetAddress() 确定实现该函数的 facet 地址(请注意,facetAddress() 可以是代理逻辑的一部分,也可以位于另一个 facet 内部并被 delegatecall 调用——我们将在后面讨论这一点)。随后,代理使用接收到的相同 calldata 对该地址进行 delegatecall。

EIP-2535 并没有规定如何将 function selector 映射到实现合约的地址。对于静态钻石,合理的解决方案是硬编码这种关系。这将是我们在下一节中采用的方法。
对于可升级钻石,显然不能硬编码 selector,我们必须依赖 mapping,这将在相应的章节中看到。
基于 Function Selector 的条件分支
下面我们展示一个具有两个实现合约(facets)的代理合约:
- 第一个实现合约
Add暴露了一个单一的 public 函数add(),该函数返回其参数的和。 - 第二个实现合约
Multiply暴露了两个 public 函数multiply()和exponent(),其功能如其名称所示。
下面的代码展示了使用两个实现合约的单一代理。Diamond 中的 facetAddress() 函数接收 msg.sig,并返回实现具有该签名函数的 facet 地址(如果有的话)。以下代码尚未符合钻石模式标准,但我们将逐步演进代码使其符合标准。
// first implementation contract
contract Add {
// selector: 0x771602f7
function add(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x + y;
}
}
// second implementation contract
contract Multiply {
// selector: 0x165c4a16
function multiply(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x * y;
}
// selector: 0x2f8cd8b1
function exponent(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x ** y;
}
}
// proxy contract
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), "Function does not exist");
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 标准并未规定调用不存在函数时的错误信息。在我们的例子中,我们使用字符串 "Function does not exist" 进行 revert,但使用 custom error 在Gas 效率上会更高。
为了简单起见,我们的 diamond 在 constructor 中部署了这两个 facets,但在实际应用中并不会这样做。在实践中,facets 是单独部署的,稍后会以某种方式(例如通过构造函数参数或单独的函数)“通知”代理它们的存在。
一些 diamond 将 facets 实现为带有 external 函数的 library(库)而不是合约,但 EIP-2535 并不强制要求 facets 必须是 library 或合约。
为了简单起见,我们使用一系列 else-if 语句将 function selector 匹配到 facet 地址,但如果选项很多,这种做法在 Gas 上是不高效的。对于静态钻石,提前对 selector 进行排序,然后进行二分查找(binary search)会更高效——但我们将稍后讨论这一点。
边界情况 —— 以太币转账 (Ether transfers)
当转移以太币时,将没有 calldata。在这种情况下,msg.sig 将返回 0x00000000,且无法映射到 facet 地址。如果合约不打算接收以太币,这是可以的。然而,如果合约预期接收以太币,或者预期对传入的以太币做出反应,那么 0x00000000 应该映射到具有预期逻辑的函数,或者至少是一个不会触发 revert 的函数。请记住,攻击者既可以通过发送空 calldata,也可以通过发送 0x00000000 作为 calldata 来触发此函数,因此逻辑需要优雅地处理这两种情况。
为了使我们的合约符合 EIP-2535 标准,我们必须实现四个强制性的 public view 函数,下面将逐一讨论。
四个强制性的 Public View 函数
1/4 facetAddress()
请注意 facetAddress() 是 public 的——EIP-2535 要求 Diamond 代理暴露一个具有以下签名的 public 函数:
function facetAddress(bytes4 selector) external view returns (address);
在这方面我们已经符合要求了。
2/4 facetAddresses()
除了 facetAddress(bytes4 selector) 之外,EIP-2535 强制要求一个名为 facetAddresses()(复数)的函数,该函数返回 diamond 使用的所有 facets——换句话说,一个地址列表。其签名如下:
function facetAddresses() public view returns (address[] memory addresses);
由于我们 diamond 上的 facets 是不可变的,我们只需硬编码 facet 地址列表即可:
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);
}
// ┌────────────────────┐
// │ │
// │ THIS CODE IS NEW │
// │ │
// └────────────────────┘
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), "Function does not exist");
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()) }
}
}
}
facetAddresses() 返回的地址列表(facet 地址)没有强制的排序要求。
3/4 facetFunctionSelectors()
给定一个 facet 地址作为参数,facetFunctionSelectors() 返回该 facet 所有 public 函数的 selectors。它具有以下签名:
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;
}
// ┌────────────────────┐
// │ │
// │ THIS CODE IS NEW │
// │ │
// └────────────────────┘
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), "Function does not exist");
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(),并返回一个结构体列表,其中每个结构体包含一个 facet 地址以及该 facet 的 function selectors 列表。
它具有以下签名:
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
function facets() external view returns (Facet[] memory facets_);
facets() 返回的信息可以通过以下方式获取:
- 首先调用
facetAddresses()获取所有 facet 地址- 遍历每个 facet 地址,并将该地址放入
Facet结构体的facetAddress字段中- 对该地址调用
facetFunctionSelectors(),并将 function selectors 列表放入结构体的functionSelectors字段中。
- 对该地址调用
- 遍历每个 facet 地址,并将该地址放入
另一种选择是直接在函数中硬编码答案,这在某些情况下可能更高效(对于可升级钻石,硬编码显然行不通)。在我们的 diamond 中,我们将使用循环方法实现 facets(),如下所示。滚动到此代码块的底部查看新代码:
contract Add {
// selector: 0x771602f7
function add(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x + y;
}
}
contract Multiply {
// selector: 0x165c4a16
function multiply(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x * y;
}
// selector: 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 empty array for unknown facets
return new bytes4[](0);
}
// ┌────────────────────┐
// │ │
// │ THIS CODE IS NEW │
// │ │
// └────────────────────┘
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), "Function does not exist");
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()) }
}
}
}
实现了这四个 public 函数后,我们现在已经实现了钻石代理的所有强制性 external 函数。
IDiamondLoupe
这四个函数共同定义在一个名为 IDiamondLoupe 的接口中。所有的 diamonds 必须实现 IDiamondLoupe。你可以这样记忆:“loupe”是指用于观察(“查看”)钻石的小型放大镜,而这些函数每一个都是 view 函数。
interface IDiamondLoupe {
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
/// @notice Gets all facet addresses and their four byte function selectors.
/// @return facets_ Facet
function facets() external view returns (Facet[] memory facets_);
/// @notice Gets all the function selectors supported by a specific facet.
/// @param _facet The facet address.
/// @return facetFunctionSelectors_
function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);
/// @notice Get all the facet addresses used by a diamond.
/// @return facetAddresses_
function facetAddresses() external view returns (address[] memory facetAddresses_);
/// @notice Gets the facet that supports the given selector.
/// @dev If facet is not found return address(0).
/// @param _functionSelector The function selector.
/// @return facetAddress_ The facet address.
function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);
}
距离完全符合标准的钻石代理,我们还有一步之遥:记录对 diamond 所做的更改。这就是我们接下来要讨论的主题。
DiamondCut —— 记录 facets selectors
我们贯穿始终的 diamond 示例是不可升级的。而在实际中,diamonds 是可升级的,可以更改其 facets 以及与其关联的 function selectors。
facet 的任何更改都必须被记录——即使对于不可升级(静态)的 diamonds,变更(添加 facets 和 function selectors)也会在部署时发生,因此需要被记录。
记录 facet selectors 背后的原理是,应该有两种方法来确定 diamond 支持的 function selectors:
- 使用上述的 public 函数,或者
- 解析日志。
即使像我们这样硬编码的 facets 也必须被记录,因此必须触发事件。在我们的例子中,事件的触发必须在部署期间进行。这类日志是标准所要求的。
当我们添加一个 facet(或进行任何更改,如替换或移除)时,该操作被称为 diamond cut(钻石切割)。
Diamond cut 并不意味着移除 facets,尽管“切割”(cutting)这个词可能有这层含义。它对应的是以某种方式改变 diamond。对 facet 的任何更改都需要触发 DiamondCut 事件,该事件将在接下来的代码块中定义。(记住这个“切割”术语的一个好方法是:当实际的钻石原石被“切割”时,它会在切割处增加一个额外的面——即 facet)。
DiamondCut 事件定义在一个名为 IDiamond 的新接口中。每次向某个 facet 添加、替换或移除函数时,都必须触发事件。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[] 是一个列表,因此可以一次性更改多个 facets。
与 Transparent Upgradeable Proxies 或 UUPS Proxies 等其他代理模式不同,diamonds 没有通过更改 facet 地址来进行升级的机制。当某个 facet(实现合约)的所有关联 function selectors 被移除时,该 facet 即被移除。当添加具有新实现地址的 function selector 时,即隐式地添加了 facets。
_init 和 _calldata 参数的作用与 OpenZeppelin Initializers 相同——我们稍后将对此进行更多讨论。如果不需要初始化数据,则 _init 应为 address(0),且 _calldata 应为 "" 或空字节。
让我们更新我们的 diamond,以便在构造函数中触发此事件:
contract Add {
// selector: 0x771602f7
function add(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x + y;
}
}
contract Multiply {
// selector: 0x165c4a16
function multiply(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x * y;
}
// selector: 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());
// ┌────────────────────┐
// │ │
// │ THIS CODE IS NEW │
// │ │
// └────────────────────┘
// there are a total of 3 facets:
// [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;
// add to _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;
// add to _diamondCuts
_diamondCuts[1].action = Add;
_diamondCuts[1].functionSelectors = _mulFacets;
// Note that the IDiamondLoupe interface functions are also logged.
_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;
// add to _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 empty array for unknown facets
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), "Function does not exist");
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 函数存储在一个单独的 facet 中,但为了简单起见,我们目前将它们放在了 diamond 内部。
无论 IDiamondLoupe 的函数存储在哪里——无论是在 diamond 本身还是在另一个 facet 中,标准都要求触发(emit)它们的地址、发生的操作(添加 facet)以及 function selectors。
IDiamondLoupe 与 DiamondCut 事件之间的重复
该 EIP 具争议的方面之一是,既可以通过解析过去的日志,也可以通过调用 IDiamondLoupe 中的 view 函数来确定 function selectors。这就导致了完成同一件事存在重复的逻辑。
通过 public 函数暴露相同数据背后的考量是,这使得与区块浏览器及其他外部系统的集成变得更容易。此外,升级脚本可以在注册新的 function selector 之前,以原子操作的方式检查它是否已经存在。而 DiamondCut 事件的意图则是为了展示升级的历史记录。
在 ERC-1967 中,区块浏览器可以查询存储槽并立即识别出逻辑合约的位置——区块浏览器不需要去解析 ERC-1967 触发的包含相同信息的日志。
使钻石实现可升级,实现 diamondCut 函数
EIP-2535 标准建议添加如下所示的 diamondCut() 函数,通过该函数可以添加、更改或移除 facets。
interface IDiamondCut is IDiamond {
/// @notice Add/replace/remove any number of functions and optionally execute
/// a function with delegatecall
/// @param _diamondCut Contains the facet addresses and function selectors
/// @param _init The address of the contract or facet to execute _calldata
/// @param _calldata A function call, including function selector and arguments
/// _calldata is executed with delegatecall on _init
function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external;
}
该标准并不强制要求升级函数必须命名为 diamondCut(),也不要求必须实现上述签名。例如,开发者可以使用自定义的函数 changeTheFacets()——但该函数必须根据其所做的 facet 更新类型触发 DiamondCut 事件。
facets 和 selectors 的数据结构
为了存储 function selectors 和 facets,我们至少需要一个从 function selectors 到 facet 地址的 mapping:
mapping(bytes4 => address) facetToSelectors;
这种数据结构使我们能够:
- 根据接收到的
msg.sig计算出应该向哪个实现合约发起 delegatecall - 确定是否可以添加新的 function selector 而不会与预先存在的 function selector 发生冲突。我们必须要求
facetToSelectors[selector] == address(0) - 确定是否可以替换或移除某个 selector。使用相同的检查,我们可以确定是否在替换或移除一个不存在的 selector(此操作应该 revert)。
实现 IDiamondLoupe 的函数
回顾一下,以下是我们需要支持的 IDiamondLoupe 中的 view 函数:
facetAddress(bytes4 selector):给定 function selector,返回 facetfacetAddresses():返回所有 facet 地址facetFunctionSelectors(address facet):给定地址,返回所有 function selectorsfacets():返回所有 facet 地址及其 function selectors
下面是我们实现每一个函数的方法:
- 函数
facetAddress(bytes4 selector)可以直接是facetToSelectorsmapping 的 view 函数——或者我们可以将该 mapping 设为 public。 - 为了在
facetAddresses()中返回所有地址,我们必须:- 显式地将所有地址存储在一个列表中
- 显式地将所有 function selectors 存储在一个列表中,遍历 function selectors,并对每一个调用
facetAddress(bytes4 selector),然后构建一个唯一地址的列表。
- 为了通过
facetFunctionSelectors(address facet)返回一个 facet 的所有 function selectors,我们必须:- 创建一个 mapping
mapping(address facet => bytes4[]),用于存储与每个 mapping 关联的 function selectors 列表 - 维护一个包含所有 function selectors 的数组,并遍历整个数组,对每一个调用
facetAddress(bytes4 selector)。如果返回的facet与facetFunctionSelectors参数中的facet相同,则将该地址添加到列表中。
- 创建一个 mapping
- 由于
facets()返回的信息与遍历facetAddresses()并对每个地址调用facetFunctionSelectors(address facet)相同,因此我们省略对实现facets()的进一步讨论。
这里存在一个根本性的权衡。如果我们使用更多的数据结构,在链上调用这些 view 函数的成本会更低,因为它们不必重新“重构”这些函数所返回的数据,但是在升级期间需要更新的数据结构也就更多。所以我们必须在这两者之间做出选择:
- 升级更便宜,但在链上调用 view 函数更昂贵。
- 调用 view 函数更便宜,但升级 facet 需要更多的记录工作(增加 Gas 成本)。
在链上调用任何 IDiamondLoupe view 函数是非常罕见的,因为它们主要供链下消费使用。因此,倾向于使用较少的数据结构是更好的选择。
facets 和 selectors 的状态变量 —— 以及避免存储冲突
如前所述,为了存储 selector 和地址信息,我们至少需要类似如下的 mapping:
mapping(bytes4 => address) facetToAddress;
在 diamond 中设置后,分配给第一个存储槽(storage slot)的 facet 中的任何 mapping 都可能会与此 mapping 发生冲突。
钻石代理中的存储冲突比其他代理模式更加复杂,因为冲突不仅可能发生在同一实现合约的升级过程中,还可能发生在不同 facets 之间。
钻石模式并没有规定应如何管理存储。处理冲突的一个直接有效的方法是使用存储命名空间(storage namespaces)。有关使用命名空间的详细解释,请参阅 EIP-7201 storage namespaces。
作为复习,在存储命名空间中,合约的状态变量被组合到一个结构体(struct)中,该结构体的基址存储在一个伪随机的槽中,通常由字符串的哈希值决定。因此,每个合约都有自己的基存储槽,从而使得存储冲突的可能性极小。
EIP-7201 源于钻石模式早期提出的一种称为“diamond storage”(钻石存储)的解决方案。EIP-2535 还提出了另一种名为“App Storage”(应用存储)的模式。不过,EIP-2535 并没有强制要求如何管理存储,因此我们只向读者推荐一种可行的解决方案,即使用 EIP-7201。有兴趣的读者可以直接从 EIP 中了解“diamond storage”和“app storage”模式——这两种模式均得到了 EIP 作者的推荐。
运行 diamond 所需的最小存储
如果我们选择保留运行 diamond 所需的最少存储,代理的命名空间应该是一个具有以下字段的结构体:
- 我们需要一个 selectors 的列表,即
bytes4[] selectors。每次添加 selector 时,我们都会扫描此列表,以确保我们没有添加以前存在的 selector。 - 我们至少需要一个从 selectors 到地址的 mapping。不过,如果也将 selectors 映射到它们在
selectors数组中的索引也会很有帮助。这样,当我们移除一个 selector 时,我们可以快速查找到它在数组中的索引。然后,我们将该条目与最后一个条目交换,并从列表中 pop 掉。因此,与其存储selector => address,不如存储一个包含地址和 selector 在数组中位置的结构体。因此,我们的 mapping 存储为selector => (address, index_in_selectors)。
下面的代码实现了上述两点:
selectors就是 selectors 列表FacetAddressAndSelectorPosition结构体存储了 facetAddress 以及该 selector 在selectors中的索引位置
struct FacetAddressAndSelectorPosition {
address facetAddress;
uint16 selectorPosition; // index of the selector in `selectors`
}
struct DiamondStorage {
mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
bytes4[] selectors;
}
可以使用 EIP-7201 中相同的模式来访问该结构体。
使用 LibDiamond 管理存储
为了保持简单,保存上述信息的结构体以及将存储指针设置到该结构体的函数可以保留在一个名为 LibDiamond 的单独 library(库)中。该库提供了一个 diamondStorage() 函数,返回指向结构体的指针,以及 facetAddress(bytes4 selector)。在这个库中定义 facetAddress 是可选的,纯粹是为了方便。
// ┌────────────────────┐
// │ │
// │ CODE FOR STORAGE │
// │ │
// └────────────────────┘
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 {
// change the slot of the storage pointer
ds.slot := position
}
}
function facetAddress(bytes4 _functionSelector)
external
override
view
returns (address facetAddress_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddress_ = ds.facetAddressAndSelectorPosition[_functionSelector].facetAddress;
}
}
对于可升级的 diamonds,常规做法是将 IDiamondLoupe 函数保留在它们自己的 facet 中,而不是在代理本身中。对该合约的命名没有要求,但 DiamondLoupeFacet 具有相当好的描述性。下面我们展示了如何使用 LibDiamond 库来实现作为 IDiamondLoupe 一部分的 external 函数 facetAddress 的 DiamondLoupeFacet。
import { LibDiamond } from "./libraries/LibDiamond.sol";
// ┌─────────────────────┐
// │ │
// │ DiamondLoupeFacet │
// │ │
// └─────────────────────┘
contract DiamondLoupeFacet is IDiamondLoupe {
/// @notice Gets the facet address that supports the given selector.
/// @dev If facet is not found return address(0).
/// @param _functionSelector The function selector.
/// @return facetAddress_ The facet address.
function facetAddress(bytes4 _functionSelector)
external
override
view
returns (address facetAddress_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddress_ = ds.facetAddressAndSelectorPosition[_functionSelector].facetAddress;
}
// other functions not shown
}
钻石标准的参考实现
EIP-2535 的作者 Nick Mudge 在以下代码库中维护了三个参考实现(diamond-1、diamond-2 和 diamond-3):
https://github.com/mudgen/diamond
这些实现针对我们之前讨论过的权衡进行了优化:如果 IDiamondLoupe 中的 view 函数在链上查询便宜,那么它们在更新时就会很昂贵,反之亦然。
Diamond-1 和 diamond-2 尽可能使用最少的存储来跟踪 facets 和 selectors,仅使用 function selectors 列表和从 function selector 到 facet 地址的 mapping。下面我们看到的是 diamond-1 的存储结构。
请注意,参考实现将 function selectors 的数组实现为从 uint256 => bytes32 选择器槽的 mapping,从而将 8 个 function selectors 打包(pack)到一个存储槽中。Mappings 比数组在 Gas 上稍微高效一些,因为它们在执行查找前不会隐式地检查数组的长度。这个“数组”的长度单独存储为 selectorCount。
struct DiamondStorage {
// function selector => facet address and selector position in selectors array
mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
bytes4[] selectors;
mapping(bytes4 => bool) supportedInterfaces;
// owner of the contract
address contractOwner;
}
另一方面,diamond-3 显式地存储了 facet 地址以及一个从 facet 地址到其存储的 function selectors 列表的 mapping:
struct FacetAddressAndPosition {
address facetAddress;
uint96 functionSelectorPosition; // position in facetFunctionSelectors.functionSelectors array
}
struct FacetFunctionSelectors {
bytes4[] functionSelectors;
uint256 facetAddressPosition; // position of facetAddress in facetAddresses array
}
struct DiamondStorage {
// maps function selector to the facet address and
// the position of the selector in the facetFunctionSelectors.selectors array
mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition;
// maps facet addresses to function selectors
mapping(address => FacetFunctionSelectors) facetFunctionSelectors;
// facet addresses
address[] facetAddresses;
// Used to query if a contract implements an interface.
// Used to implement ERC-165.
mapping(bytes4 => bool) supportedInterfaces;
// owner of the contract
address contractOwner;
}
diamond-3 的 DiamondLoupe 实现非常简单,因为它只是这些状态变量的一个薄层封装(thin wrapper):
contract DiamondLoupeFacet is IDiamondLoupe, IERC165 {
// Diamond Loupe Functions
////////////////////////////////////////////////////////////////////
/// These functions are expected to be called frequently by tools.
//
// struct Facet {
// address facetAddress;
// bytes4[] functionSelectors;
// }
/// @notice Gets all facets and their selectors.
/// @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 < numFacets; i++) {
address facetAddress_ = ds.facetAddresses[i];
facets_[i].facetAddress = facetAddress_;
facets_[i].functionSelectors = ds.facetFunctionSelectors[facetAddress_].functionSelectors;
}
}
/// @notice Gets all the function selectors provided by a facet.
/// @param _facet The facet address.
/// @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 Get all the facet addresses used by a diamond.
/// @return facetAddresses_
function facetAddresses() external override view returns (address[] memory facetAddresses_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddresses_ = ds.facetAddresses;
}
/// @notice Gets the facet that supports the given selector.
/// @dev If facet is not found return address(0).
/// @param _functionSelector The function selector.
/// @return facetAddress_ The facet address.
function facetAddress(bytes4 _functionSelector) external override view returns (address facetAddress_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddress_ = ds.selectorToFacetAndPosition[_functionSelector].facetAddress;
}
// This implements ERC-165.
function supportsInterface(bytes4 _interfaceId) external override view returns (bool) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
return ds.supportedInterfaces[_interfaceId];
}
}
diamond-1 和 diamond-2 的 view 函数更加复杂,因为它们必须遍历所有的 function selectors 才能列出 facet 地址,然后仅返回唯一的地址。
部署 diamond
部署和升级 diamond 的过程与其他可升级代理模式相同:
在部署期间,我们:
- 部署 facets(实现)
- 使用 facets 列表和 selectors 作为构造函数的参数来部署代理,并在同一笔交易中进行初始化
在升级期间:
- 部署新的 facet(s)
- 调用 diamondCut() 或用户定义的函数,并传入要移除的 selectors 列表和要添加的 selectors 列表。可以在单笔交易中添加多个新 facets。
在部署和升级期间初始化状态变量
当部署代理时,我们可能希望像有构造函数一样设置一些初始状态变量。同样,在升级之后,我们可能也想初始化某些状态变量,这类似于 Transparent Upgradeable Proxy 和 UUPS 的 OpenZeppelin 实现中的 upgradeToAndCall。
这就是 diamondCut 中最后两个参数 _init 和 _calldata 的用途:
function diamondCut(Facets[] facets, address _init, bytes memory _calldata)
如果 _init ≠ address(0),那么 diamondCut 必须使用 _calldata 作为参数向 _init 发起 delegatecall。由于 diamondCut 运行在代理(diamond)的上下文中,被 delegatecall 调用的合约(_init)可以初始化代理中的状态变量。
初始化逻辑由外部合约运行。如果我们想在单笔交易中设置多个状态变量,通过使用一个专用智能合约作为单笔交易来完成会更容易。
示例合约如下所示:
import {LibDiamond} from "../libraries/LibDiamond.sol";
contract DiamondInit {
function init() external {
// read the ds struct from storage
// (remember, this executes in the context of the proxy)
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
// write to it
ds.owner = 0x456....;
ds.usdc = 0xa1...;
// ...
}
}
对 diamondCut 的调用将传递 DiamondInit 的地址以及 init() 的 ABI 编码。
初始化可以发生在任何执行 diamondCut 交易的时候——并不局限于 facets 的初始部署。当替换或移除 function selector 时,我们同样可以以原子操作的方式对外部合约发起 delegatecall。
考虑在使用 DiamondInit 时要求一个 Magic Value
EIP-2535 并没有要求对 DiamondInit 合约进行安全检查以确保它确实是一个合约,但检查 _init 的地址是否确实拥有字节码显然是更安全的做法。为了增加安全性,ZkSync’s diamond implementation 会检查向 _init 合约发起的 delegatecall 是否返回一个 magic value(魔数/魔法值)。
钻石标准的实现细节
使用钻石标准最安全的方法是使用上面链接的参考合约,因为这些合约已经过审计。如果你不打算在链上调用 IDiamondLoupe 函数(通常也是这种情况),那么 diamond-2 将是 Gas 效率最高的,因为它使用了符合 ERC-2535 所需的最少数量的状态变量。
不可升级 diamonds —— 使用二分查找代替 mapping
对于不可升级的 diamonds,我们建议硬编码 function selectors 之间的关系。与其使用冗长的 if-else 语句来查找 facet 地址,不如使用二分查找(binary search)。
举个例子,考虑下面的函数,它将 function selector 作为参数并返回一个地址(该代码取自使用了钻石代理的 Pendle Finance):

该代码对 function selector 进行二分查找,并返回包含该函数的实现合约(facet)的地址。仅此而已。
上述地址是在构造函数中设置的不可变地址,构造函数还会根据要求触发 DiamondCut 事件:

不要使用写入状态变量的库,除非它们使用了 EIP-7201
任何向非命名空间(non-namespaced)存储写入或读取数据的 facet 都容易发生存储冲突。我们建议使用 EIP-7201 来管理所有的存储。
在另一个 facet 中调用函数
当 facet(实现合约)中的函数运行时,它是在 diamond(代理合约)的上下文中执行的。因此,要调用另一个 facet 中的 public 函数,我们可以调用代理地址。
回想一下我们最初的例子,我们有一个带有 add() 函数的 facet 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);
// rest of the code
}
}
在底层,这将执行两次调用:
- 首先
callAdd调用自身(在代理的上下文中) - 代理将 function selector 匹配到 Add facet
- 代理向 Add facet 发起 delegatecall
合约能以这种方式调用自身可能会让一些开发者感到惊讶,但这在 EVM 中确实是允许的。
你可以测试以下合约来证明这一点:
contract SelfCall {
uint256 public x = 0;
function setToOne() external {
x = 1;
}
function selfCall() external {
SelfCall(address(this)).setToOne();
// alternatively,
// address(this).call(abi.encodeWithSignature("setToOne()"));
}
}
然而,进行自调用有些浪费。为了节省 Gas,一个 facet 可以“直接”对另一个 facet 发起 delegatecall。在幕后,调用方 facet 的逻辑在代理中运行,并且该逻辑正向另一个 facet 发起 delegatecall,因此该 facet 可以“看到”代理中的状态变量。实现这一点的代码不会那么简洁,因为 delegatecall 只能是底层调用(low-level call)。这是取自 EIP-2535 的一个例子:
// get the mapping from selector => facet address
DiamondStorage storage ds = diamondStorage(); // EIP-7201
// compute the selector
bytes4 functionSelector = bytes4(keccak256("functionToCall(uint256)"));
// get the facet address
address facet = ds.selectorToFacet[functionSelector];
// delegatecall
bytes memory myFunctionCall = abi.encodeWithSelector(functionSelector, 4);
(bool success, bytes memory result) = address(facet).delegatecall(myFunctionCall);
然而,上面的代码不够优雅,需要额外写三到四行代码才能完成一个简单的调用。
第三种解决方案是创建仅包含 internal 函数的 Solidity 库,然后在任何需要它们的 facet 中导入这些 internal 函数。这可能会导致 facets 之间出现重复的字节码,但如果 facets 没有超过 24kb 的限制,或者没有过度增加部署成本,这就不应该是个大问题。
Diamond 示例
我们将构建一个用作计数器合约的 diamond。一个 facet 将持有计数器值的 view 函数,另一个 facet 将持有增加计数器的逻辑。
为了演示 diamond 初始化逻辑,我们将让计数器从 8 而不是 0 开始。
首先,fork diamond-2 reference hardhat repository 项目。据我们所知,目前没有任何经过审计的 Foundry 实现。
IncrementLibrary
将读取命名空间存储的代码放在 facets 可以导入的 library 中是很有帮助的。将更新计数器的逻辑放入 library 中是一种设计选择。将增量逻辑直接放在 library 中会增加每一个调用该 library 函数的其他 facet 的字节码大小。不过,这也简化了 increment facet 的逻辑,因为 increment facet 不需要了解有关命名空间存储结构的任何信息。
请注意,此 library 中的所有函数必须是 internal 的,因为 Solidity 编译器期望具有 external 函数的 library 能够被单独部署。
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() 调用的合约向其发起 delegatecall。在参考实现中,它位于 contracts/upgradeInitializers/DiamondInit.sol。
为了简洁起见,我们不展示整个合约。将以下代码添加到 DiamondInit.sol 中:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// ADD THIS LINE
import {LibInc} from "../libraries/LibInc.sol";
//...
contract DiamondInit {
// You can add arguments to this function in order to pass in
// data to set your own state variables
function init() external {
// ...
// ADD THESE LINES
LibInc.IncrementStorage storage _is = LibInc.incStorage();
_is.x = 8; // initialize x to 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 钩子(hook),以部署新 facets。
顺带一提:Solidity 编译器能够自动输出 public/external 函数的 function selectors。例如,假设我们在 C.sol 中存储了以下合约:
contract C {
function foo() public {}
function bar() external {}
}
如果我们在该合约上运行 solc --hashes C.sol,我们将得到以下输出:
======= C.sol:C =======
Function signatures:
febb0f7e: bar()
c2985578: foo()
该代码库中提供的脚本使用这种技术为我们提取 function selectors,这就省去了我们亲自显式指定 function selectors 的麻烦。
同样,hardhat 能够在不指定文件路径的情况下找到合约:

运行以下命令进行测试:
npx hardhat test
请注意,其他测试将会失败,因为它们并未预期出现新的 selectors。我们在测试中使用 .only 修饰符来忽略这一点。.only 修饰符会阻止运行其他单元测试,仅运行带有 .only 修饰符的单元测试。
如果你遇到问题,请确保你使用的是 Node 版本 20。
总结
- 钻石模式是一种具有多个实现合约的代理。
- diamond 根据传入 calldata 的 function selector 知道该向哪个 facet 发起 delegatecall。
- 如果 diamond 是可升级的,从 selector 到实现地址的 mapping 可以通过 diamondCut 更改。更改此 mapping 的逻辑并未在标准中规定,它可以是代理字节码的一部分,也可以是某个 facet 的一部分。
- diamond 中的所有 facets 和 selectors 必须能够通过触发的事件和 IDiamondLoupe 中的 public 函数来确定。
- 可以通过添加更多数据结构来降低查看
IDiamondLoupe函数的 Gas 成本。但这会增加升级成本。在几乎所有情况下,我们都应选择较少的数据结构,因为IDiamondLoupe函数是为链下用户准备的。
我们要感谢 Nick Mudge 为本文的早期版本提供反馈意见。