EIP-2535 钻石合约

什么是 Diamond

Diamond 指的是一种设计模式,称为 EIP-2535 Diamonds。这种模式用于构建可模块化和可升级的以太坊智能合约。

Diamond

  • 模块化架构:Diamond 是一种创建单一、模块化合约(称为 Diamond)的方式,该合约将不同的功能委托给分离的、可互换的合约组件(称为 Facets)。这有助于更好地组织代码和分离关注点。
  • Facets:每个 Facet 包含特定的功能,并可以独立升级。例如,一个 Facet 可能处理用户管理,而另一个处理交易。
  • Diamond Storage:在 Diamond 模式中,合约的存储结构被设计成可以支持不同 Facets 的升级,而不影响整体合约的其他部分。

对外部世界(如用户界面、其他智能合约和软件/脚本)而言,Diamond 看起来是一个单一的智能合约,具有一个单一的以太坊地址。但在内部,它使用一组称为 Facets 的合约来处理其外部功能,这些 Facets 对外部是隐藏的。

当外部软件程序(如其他智能合约或用户界面)对 Diamond 发起函数调用时,Diamond 会检查是否有包含该函数的 Facet,如果存在,则使用该 Facet。

要理解 Diamonds,需要了解以下几点:

  • Diamond 是一个智能合约:其以太坊地址是外部软件与之交互的唯一地址。
  • 内部结构:Diamond 内部使用一组称为 Facets 的合约来处理外部函数。
  • 状态变量存储:所有的状态变量存储数据都保存在 Diamond 中,而不是在 Facets 中。
  • 数据访问:Facets 的外部函数可以直接读取和写入存储在 Diamond 中的数据。这使得 Facets 编写起来简单且节省 Gas。
  • 实现机制:Diamond 通常通过一个 fallback 函数实现,该函数使用 delegatecall 将外部函数调用路由到 Facets。
  • 外部函数:Diamond 本身通常没有任何外部函数——它使用 Facets 处理外部函数,这些函数读取/写入 Diamond 的数据。

EIP-2535 规范了添加、替换、移除函数和 Facets 的机制和合约接口,以及检查 Diamond 以了解其拥有的函数和 Facets 的方法,并记录升级。EIP-2535 使得可以编写能够与任何和所有 Diamonds 兼容或集成的软件。

以下是帮助可视化 Diamond 的几张图片。第一张图片展示了函数如何映射到持有函数代码的 Facets 上:

Diamond

接下来的图片展示了所有状态变量数据都存储在 Diamond 代理合约中,而 Diamond 使用的代码来自 Facets。请注意,虽然 Facets 中的代码可以定义具有状态变量的结构体,但所有存储的值都保存在 Diamond 代理合约中,而不是在 Facets 中。该图片还显示了 Facets 可以在 Diamond 代理合约中拥有自己的数据,并且可以与其他 Facets 共享数据。这张图片只是一个示例。

Diamond

为 Diamond 组织 Facet

Facets 可以像文件系统一样进行组织。文件系统的常见组织方式如下:

  • 相关和相似的数据放在同一个文件中:例如,将所有与用户管理相关的函数放在一个 Facet 中。
  • 相关和相似的文件放在同一个文件夹中:例如,将所有用户管理相关的 Facets 放在同一个 Facet 文件夹中。
  • 相关和相似的文件夹放在同一个父文件夹中:例如,将所有与用户管理和交易处理相关的 Facets 放在一个更大的父 Facet 文件夹中。

这种组织方式有助于提高代码的模块化和可维护性,使得不同功能的 Facets 更加清晰和易于管理。

Diamond 的 Facets 也可以类似地进行组织:

  • 相似和相关的函数放在同一个 Facet 中:例如,将所有用户管理相关的函数放在一个 Facet 中,以保持功能的集中性。
  • 相似和相关的 Facets 放在同一个文件夹中相似和相关的 Facets 放在同一个文件夹中:例如,将所有与用户管理相关的 Facets 放在一个特定的 Facet 文件夹中,这样可以更好地组织和管理。
  • 相似和相关的文件夹放在同一个父文件夹中:例如,将所有与用户管理和交易处理相关的 Facets 放在一个更大的父 Facet 文件夹中,便于整体结构的管理和扩展。

例如,在实现 ERC721 代币时,可以将 ERC721 标准中的外部函数实现放在一个名为 ERC721Facet 的 Facet 中,而将自定义功能放在另一个 Facet 中。

即使 24KB 的最大合约限制被移除,我仍然会使用 EIP-2535 Diamonds,因为它提供了一种系统化的方式来组织、升级和扩展智能合约系统。

最初,EIP-2535 Diamonds 是为了解决 24KB 合约限制而创建的,但它的应用范围远不止于此。它提供了一个构建更大规模智能合约系统的框架,并且能够支持生产环境中不断增长的合约系统。

确保数据准确

Solidity 在合约中使用数字地址空间来存储数据。第一个状态变量存储在位置 0,下一个状态变量存储在位置 1,再下一个状态变量存储在位置 2,依此类推。

Diamond 的 Facets 共享相同的存储地址空间,因为它们具有相同的 Diamond,且 Facets 仅读取和写入 Diamond 中的状态变量,而不是它们自己。如果不了解这一点,你需要理解 delegatecall 的工作原理,因为所有 Facet 的外部函数都是通过 delegatecall 从 Diamond 调用的。

如果处理不当,这可能会引发问题。例如,假设一个 Diamond 具有两个 Facets:FacetA 和 FacetB。假设 FacetA 声明了状态变量 uint first; 和 bytes32 second;,而 FacetB 声明了状态变量 uint first; 和 string name;。

由于两个 Facets 都在位置 0 存储 uint first,因此它们都可以读取和写入这个变量而不会有问题。

但这两个 Facets 在位置 1 写入和读取的是不同的数据——‘bytes32 second’ 和 ‘string name’。它们会互相覆盖或混淆彼此的数据,因为它们在位置 1 解释和写入的数据不同。这就是为什么同一个 Diamond 的 Facets 需要以相同的顺序声明相同的状态变量,以确保它们读取和写入相同的位置。

要解决这个问题,需要有策略来确保 Facets 声明相同的状态变量,并保持相同的顺序。即使在进行升级时,只要有良好的策略,这通常没有问题。

Inherited Storage

创建一个合约来声明所有 Facets 使用的状态变量,并让每个 Facet 继承这个合约(例如,命名为 Storage),是一种简单的策略。这种策略确实有效,并且在生产环境中已经成功使用。然而,它也有一些限制。

Inherited Storage 的限制:

  • 复用性问题: 由于状态变量是通过继承合约来管理的,这种方式可能会限制 Facets 的复用。如果一个 Facet 使用了 Inherited Storage,它可能无法与具有不同状态变量的其他 Diamond 进行重用。这使得在不同 Diamond 之间迁移或共享 Facet 变得困难。
  • 命名冲突: 在大型 Diamond 中,拥有 100 个或更多状态变量时,很容易发生命名冲突。尤其是如果内部函数或局部变量与状态变量同名,就可能导致名称冲突。这种情况可能会使代码维护变得复杂。

可能的解决方案:

  • 命名约定:使用代码命名约定来防止命名冲突。例如,为状态变量、内部函数和局部变量使用不同的前缀或命名规则,以减少命名冲突的风险。
  • 模块化存储:考虑使用模块化存储策略,将状态变量分组到多个存储合约中,而不是将所有变量放在一个合约中。这种方式可以提高复用性,并减少命名冲突的风险。
  • 更灵活的存储管理:探索其他存储管理策略,例如动态存储管理合约,通过配置和管理存储布局来提高 Facets 的复用性。

通过这些改进措施,可以克服 Inherited Storage 的一些局限性,同时提高代码的复用性和维护性。

Diamond Storage

不同 Diamond 的 Facets 实际上并不需要声明相同的状态变量或以相同的顺序存储状态变量,只要 Facets 存储数据的位置不同即可。

正如前面提到的,Solidity 自动将状态变量存储在从 0 开始并递增的存储位置上。然而,我们并不一定要使用 Solidity 的默认存储布局机制。我们可以指定在地址空间中存储数据的起始位置,因此可以为不同的 Facets 指定不同的存储位置,从而避免不同 Facets 中不同状态变量发生存储位置冲突。这就是 Diamond Storage 的工作原理。

我们可以通过对唯一字符串进行哈希来获取一个随机的存储位置,并在该位置存储一个结构体。该结构体可以包含我们想要的所有状态变量。唯一字符串可以充当特定功能的命名空间。

例如,我们可以实现一个 ERC721Facet。这个 Facet 可以在位置 keccak256(“com.myproject.erc721”); 存储一个名为 ERC721Storage 的结构体。这个结构体可以包含所有与 ERC721 相关的状态变量,这些变量是 ERC721Facet 读写的,并且不包含其他变量。这样做有几个优点:

  • Facet 的复用性:ERC721Facet 可以仅部署一次,并可以与多个使用不同状态变量的 Diamond 配合使用。这种复用性使得开发和部署更加高效。
  • 简洁性:ERC721Facet 不会被不相关的状态变量声明所 clutter,保持了代码的简洁性和可维护性。只包含它实际使用的状态变量,从而避免了混乱和潜在的命名冲突。

通过这种方式,可以有效管理 Diamond 的状态变量,确保 Facets 的高效和清晰。

AppStorage

AppStorage 是一种类似于 Inherited Storage 的机制,但它解决了名称冲突的问题,在实际应用中,这一点非常有用。AppStorage 的设计不仅避免了状态变量与内部函数或局部变量的名称冲突,还通过结构化的方式提高了代码的可读性。如果你注重代码的可读性,那么你会喜欢 AppStorage。

AppStorage 强制使用命名或访问约定,避免了状态变量与其他元素名称冲突的问题。

AppStorage 的工作原理如下:

  • 定义结构体:在 Solidity 文件中定义一个名为 AppStorage 的结构体。该结构体包含将在 Facets 之间共享的状态变量。
  • 使用结构体:Facet 导入 AppStorage 结构体,并将 AppStorage internal s; 声明为 Facet 中的第一个也是唯一的状态变量。这样,Facet 中的所有状态变量都通过结构体进行访问。例如,使用 s.myFirstVariable、s.mySecondVariable 等来访问状态变量。
AppStorage.sol
1
2
3
4
5
6
7
8
9
10
// AppStorage.sol
pragma solidity ^0.8.0;

contract AppStorage {
struct Storage {
uint256 myFirstVariable;
uint256 mySecondVariable;
// Add other state variables here
}
}
MyFacet.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// MyFacet.sol
pragma solidity ^0.8.0;

import "./AppStorage.sol";

contract MyFacet {
AppStorage.Storage internal s;

function setFirstVariable(uint256 _value) external {
s.myFirstVariable = _value;
}

function getFirstVariable() external view returns (uint256) {
return s.myFirstVariable;
}

// Additional functions interacting with `s` here
}

优势

  • 避免名称冲突:通过将所有状态变量放入一个结构体中,避免了与内部函数或局部变量的名称冲突。
  • 提高代码可读性:结构化的方式使得状态变量的管理更加清晰,使代码更容易阅读和维护。

AppStorage 是一种有效的策略,用于管理和组织 Facets 中的状态变量,特别适合需要高代码可读性的场景。

代码实现

  • diamond: 合约本体,实际上是一个代理合约
  • facet: 业务合约
  • loupe: 也是一个 facet,用于查询 diamond 提供的 facet 以及 facet 提供的所有函数
  • diamondCut: 用于增加/删除/修改 diamond 上的 facet 的函数

Loupe

IDiamondLoupe.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/******************************************************************************\
* Author: Nick Mudge <[email protected]> (https://twitter.com/mudgen)
* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535
/******************************************************************************/

// A loupe is a small magnifying glass used to look at diamonds.
interface IDiamondLoupe {

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

/// 获取某个 diamond 的所有 facet
function facets() external view returns (Facet[] memory facets_);

/// 获取某个 facet 的所有函数选择器
function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);

/// 获取某个 diamond 的所有 facet 地址
function facetAddresses() external view returns (address[] memory facetAddresses_);

/// 获取某个 diamond 的某个函数选择器对应的 facet 地址
function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);
}

DiamondCut

IDiamondCut.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/******************************************************************************\
* Author: Nick Mudge <[email protected]> (https://twitter.com/mudgen)
* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535
/******************************************************************************/

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

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

/// 增加/替换/删除 facet
function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external;

/// 每次调用 DiamondCut 都需要触发该事件
event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);
}

Diamond

LibDiamond.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/******************************************************************************\
* Author: Nick Mudge <[email protected]> (https://twitter.com/mudgen)
* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535
/******************************************************************************/
import { IDiamondCut } from "../interfaces/IDiamondCut.sol";

// EIP2535 diamond 标准里 loupe 函数是必须的

error InitializationFunctionReverted(address _initializationContractAddress, bytes _calldata);

library LibDiamond {
// 32位的 keccak 哈希值,用于作为 diamond 存储位置的字符串。
bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage");

struct FacetAddressAndPosition {
address facetAddress;
uint96 functionSelectorPosition;
}

struct FacetFunctionSelectors {
bytes4[] functionSelectors;
uint256 facetAddressPosition;
}

struct DiamondStorage {
// 函数选择器到 facet 地址和选择器在 facetFunctionSelectors.selectors 数组中的位置的映射
mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition;

// facet 地址到 facet 函数选择器的映射
mapping(address => FacetFunctionSelectors) facetFunctionSelectors;

// facet 地址
address[] facetAddresses;

// 查询某个合约是否实现某个接口。用于实现 ERC-165。
mapping(bytes4 => bool) supportedInterfaces;

// 合约所有者
address contractOwner;
}

function diamondStorage() internal pure returns (DiamondStorage storage ds) {
bytes32 position = DIAMOND_STORAGE_POSITION;
// 将结构体存储槽分配给存储位置
assembly {
ds.slot := position
}
}

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

function setContractOwner(address _newOwner) internal {
DiamondStorage storage ds = diamondStorage();
address previousOwner = ds.contractOwner;
ds.contractOwner = _newOwner;
emit OwnershipTransferred(previousOwner, _newOwner);
}

function contractOwner() internal view returns (address contractOwner_) {
contractOwner_ = diamondStorage().contractOwner;
}

function enforceIsContractOwner() internal view {
require(msg.sender == diamondStorage().contractOwner, "LibDiamond: Must be contract owner");
}

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

// 内部函数版本的 diamondCut
function diamondCut(
IDiamondCut.FacetCut[] memory _diamondCut,
address _init,
bytes memory _calldata
) internal {
for (uint256 facetIndex; facetIndex < _diamondCut.length; facetIndex++) {
IDiamondCut.FacetCutAction action = _diamondCut[facetIndex].action;
if (action == IDiamondCut.FacetCutAction.Add) {
addFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors);
} else if (action == IDiamondCut.FacetCutAction.Replace) {
replaceFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors);
} else if (action == IDiamondCut.FacetCutAction.Remove) {
removeFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors);
} else {
revert("LibDiamondCut: Incorrect FacetCutAction");
}
}
emit DiamondCut(_diamondCut, _init, _calldata);
initializeDiamondCut(_init, _calldata);
}

function addFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut");
DiamondStorage storage ds = diamondStorage();
require(_facetAddress != address(0), "LibDiamondCut: Add facet can't be address(0)");
uint96 selectorPosition = uint96(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length);
// 如果 facet 地址不存在,则添加
if (selectorPosition == 0) {
addFacet(ds, _facetAddress);
}
for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
bytes4 selector = _functionSelectors[selectorIndex];
address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress;
require(oldFacetAddress == address(0), "LibDiamondCut: Can't add function that already exists");
addFunction(ds, selector, selectorPosition, _facetAddress);
selectorPosition++;
}
}

function replaceFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut");
DiamondStorage storage ds = diamondStorage();
require(_facetAddress != address(0), "LibDiamondCut: Add facet can't be address(0)");
uint96 selectorPosition = uint96(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length);
// 如果 facet 地址不存在,则添加
if (selectorPosition == 0) {
addFacet(ds, _facetAddress);
}
for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
bytes4 selector = _functionSelectors[selectorIndex];
address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress;
require(oldFacetAddress != _facetAddress, "LibDiamondCut: Can't replace function with same function");
removeFunction(ds, oldFacetAddress, selector);
addFunction(ds, selector, selectorPosition, _facetAddress);
selectorPosition++;
}
}

function removeFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut");
DiamondStorage storage ds = diamondStorage();
// 如果 facet 地址不存在,则不做任何操作并返回
require(_facetAddress == address(0), "LibDiamondCut: Remove facet address must be address(0)");
for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
bytes4 selector = _functionSelectors[selectorIndex];
address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress;
removeFunction(ds, oldFacetAddress, selector);
}
}

function addFacet(DiamondStorage storage ds, address _facetAddress) internal {
enforceHasContractCode(_facetAddress, "LibDiamondCut: New facet has no code");
ds.facetFunctionSelectors[_facetAddress].facetAddressPosition = ds.facetAddresses.length;
ds.facetAddresses.push(_facetAddress);
}


function addFunction(DiamondStorage storage ds, bytes4 _selector, uint96 _selectorPosition, address _facetAddress) internal {
ds.selectorToFacetAndPosition[_selector].functionSelectorPosition = _selectorPosition;
ds.facetFunctionSelectors[_facetAddress].functionSelectors.push(_selector);
ds.selectorToFacetAndPosition[_selector].facetAddress = _facetAddress;
}

function removeFunction(DiamondStorage storage ds, address _facetAddress, bytes4 _selector) internal {
require(_facetAddress != address(0), "LibDiamondCut: Can't remove function that doesn't exist");
// immutable function 指的是定义在 diamond 里的函数,不能被修改。
require(_facetAddress != address(this), "LibDiamondCut: Can't remove immutable function");
// 选择器位置前移,然后删除最后一个选择器
uint256 selectorPosition = ds.selectorToFacetAndPosition[_selector].functionSelectorPosition;
uint256 lastSelectorPosition = ds.facetFunctionSelectors[_facetAddress].functionSelectors.length - 1;
// 如果不相同,则替换 _selector 与最后一个选择器
if (selectorPosition != lastSelectorPosition) {
bytes4 lastSelector = ds.facetFunctionSelectors[_facetAddress].functionSelectors[lastSelectorPosition];
ds.facetFunctionSelectors[_facetAddress].functionSelectors[selectorPosition] = lastSelector;
ds.selectorToFacetAndPosition[lastSelector].functionSelectorPosition = uint96(selectorPosition);
}
// 删除最后一个选择器
ds.facetFunctionSelectors[_facetAddress].functionSelectors.pop();
delete ds.selectorToFacetAndPosition[_selector];

// 如果没有选择器了,则删除 facet 地址
if (lastSelectorPosition == 0) {
// replace facet address with last facet address and delete last facet address
uint256 lastFacetAddressPosition = ds.facetAddresses.length - 1;
uint256 facetAddressPosition = ds.facetFunctionSelectors[_facetAddress].facetAddressPosition;
if (facetAddressPosition != lastFacetAddressPosition) {
address lastFacetAddress = ds.facetAddresses[lastFacetAddressPosition];
ds.facetAddresses[facetAddressPosition] = lastFacetAddress;
ds.facetFunctionSelectors[lastFacetAddress].facetAddressPosition = facetAddressPosition;
}
ds.facetAddresses.pop();
delete ds.facetFunctionSelectors[_facetAddress].facetAddressPosition;
}
}

function initializeDiamondCut(address _init, bytes memory _calldata) internal {
if (_init == address(0)) {
return;
}
enforceHasContractCode(_init, "LibDiamondCut: _init address has no code");
(bool success, bytes memory error) = _init.delegatecall(_calldata);
if (!success) {
if (error.length > 0) {
// bubble up error
/// @solidity memory-safe-assembly
assembly {
let returndata_size := mload(error)
revert(add(32, error), returndata_size)
}
} else {
revert InitializationFunctionReverted(_init, _calldata);
}
}
}

function enforceHasContractCode(address _contract, string memory _errorMessage) internal view {
uint256 contractSize;
assembly {
contractSize := extcodesize(_contract)
}
require(contractSize > 0, _errorMessage);
}
}

实现方式:Diamond