跳到主要内容

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 / Remove
  • bytes4[] 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 对数据具有不同访问权限。

如下图所示:

diamondstorage1

  • 只有 FacetA 可以访问 DataA
  • 只有 FacetB 可以访问 DataB
  • Diamond可以访问 DataD
  • FacetAFacetB 共享对 DataAB 的访问权限
  • DiamondFacetAFacetB 共享对 DataABD 的访问权限

合约实现

完整实现参考