ERC-2535: Diamonds, Multi-Facet Proxy
ERC-2535
也被称作 钻石合约标准,是一种模块化的智能合约系统
主要有如下特点:
- 一个地址管理无限合约功能:使用单个地址来实现合约功能, 可以使部署、测试和与其他智能合约、软件和用户界面的集成更加容易。
- 允许合约大小超过 24KB
- 提供组织合约代码和数据的方法:对于构建具有许多功能的合约系统, 钻石合约提供了一种系统化的隔离这些功能的方式,并将它们连接在一起,并以高效节省 gas 的方式共享数据
- 提供了升级功能的方法:可升级的钻石合约可以进行添加/替换/删除功能的升级。由于钻石合约没有大小限制,因此可以随时向钻石添加任意数量的功能性内容。也可以在不重新部署现有功能性内容情况下对钻石进行升级处理
- 不可变性:可以部署一个不可变的钻石合约,或者在以后将可升级的钻石合约变为不可变
- 重复使用已部署的合约:不需要将合约部署到区块链上,可以使用已经部署在链上的合约来创建钻石合约
该标准是
EIP-1538
的改进版
合约接口
Diamond
: 主合约,代理合约Facet
: 逻辑合约DiamondLoupe
: 查询逻辑合约提供什么方法,以及方法对应的逻辑合约地址DiamondCut
: 提供添加/替换/删除合约功能的能力
Diamond
所有的钻石合约必须实现 IDiamond
接口
- 在部署钻石合约时,任何不可变函数和添加到钻石合约中的任何
external
函数都必须在DiamondCut
事件中发出 - 每当添加、替换或移除
external
函数时,都必须发出一个DiamondCut
事件。这适用于所有升级、所有函数更改,在任何时间,无论是通过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);
}
DiamondCut
事件记录了对钻石合约的所有功能更改。
DiamondCut
钻石合约实现应有 mapping
类型变量用来将函数 selector
映射到 Facet
合约地址
mapping(bytes4 => address) facets;
通过修改该变量,可以添加/替换/删除功能。如果在钻石合约部署后允许修改这个变量的值,则应实现 IDiamondCut
接口
interface IDiamondCut is IDiamond {
function diamondCut(
FacetCut[] calldata _diamondCut, // FacetCut 结构来自 IDiamond
address _init,
bytes calldata _calldata
) external;
}
_diamondCut
参数是 FacetCut
结构体的数组。
每个 FacetCut
结构体包含了
address facetAddress
: 逻辑合约地址FacetCutAction action
: // 行为, 可以为 Add / Replace / Removebytes4[] functionSelectors
: 逻辑合约的函数选择器数组
当 action
有三种行为:
Add
: 则将每个functionSelectors
项的函数选择器映射更新为facetAddress
。如果任何functionSelectors
具有映射的facet
,则回滚。Replace
: 则将每个functionSelectors
项的函数选择器映射更新为facetAddress
。如果任何functionSelectors
具有与facetAddress
相等的值或者选择器未设置,则回滚。Remove
: 则删除每个functionSelectors
项的函数选择器映射。如果先前有任何未设置的functionSelectors
, 则回滚。
在添加/替换/删除功能之后,使用 delegatecall
在 _init
上执行 _calldata
参数。此执行用于初始化数据
如果 _init
值为 address(0)
,则跳过 _calldata 执行
。在这种情况下,_calldata
可以包含 0 字节或自定义信息。
Facet
具体的逻辑合约实现
DiamondLoupe
通过实现 IDiamondLoupe
接口来查询逻辑合约支持的功能
interface IDiamondLoupe {
// 存储逻辑合约的地址和可调用的函数选择器数组
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
/// @notice 获取所有的逻辑合约
/// @return facets_ Facet
function facets() external view returns (Facet[] memory facets_);
/// @notice 获取某逻辑合约可调用的函数选择器数组
/// @param _facet The facet address.
/// @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 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_);
}
fallback
与代理模式类似,当钻石合约上有一个函数调用时,fallback
函数将被执行
fallback() external payable {
// 获取函数选择器, 不再由 msg.data 提供
address facet = selectorTofacet[msg.sig];
require(facet != address(0));
// Execute external function from facet using delegatecall and return any value.
assembly {
// 拷贝 calldata
calldatacopy(0, 0, calldatasize())
// 使用 delegatecall 调用 facet
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
// 获取返回值
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())}
}
}
数据存储
同代理模式一样,数据存储在钻石合约中。但不同 Facet
对数据具有不同访问权限。
如下图所示:
- 只有
FacetA
可以访问DataA
- 只有
FacetB
可以访问DataB
Diamond
可以访问DataD
FacetA
和FacetB
共享对DataAB
的访问权限Diamond
、FacetA
和FacetB
共享对DataABD
的访问权限