钻石标准主要是为了对应以太坊上智能合约大小24K字节的限制。同时也可以应用来处理智能合约的无缝升级问题。钻石合约是这样的一个合约:它将函数调用代理调用(delegatecall)到外部已经部署的合约。这样的外部已部署合约被称为钻石面(facets)
1. 标准
钻石标准定义在EIP2535中。标准文本在这里。https://github.com/ethereum/EIPs/issues/2535
2. 例子实现
下面是钻石标准的作者 Nick Mudge提供的几个标准实现
- [ diamond-1 ] ( https://github.com/mudgen/diamond-1 )
- [ diamond-2 ] ( https://github.com/mudgen/diamond-2 )
- [ diamond-3 ] ( https://github.com/mudgen/diamond-3 )
3. 概览
diamondCut 函数是用来合约升级的函数,可以增加,替代和删除钻石合约里的任意函数 。它接收一个bytes[]类型的参数输入,指明修改内部映射表所需要的方法-钻石面对。比如,调用diamondCut函数可以一次性在一个交易里增加2个新函数,替换3个函数并且删除4个函数。同时diamondCut函数可以触发事件,记录所有的增加,替换和删除。
放大镜(The Loupe)是用来查询钻石合约的内部状况。钻石合约提供4个函数来提供钻石合约当前存储的函数和钻石面。这些函数被统称为放大镜。所有的钻石合约都必须实现这些函数
4. 例子解释
下面我们以diamond-1为例来说明
4.1 数据结构
在IDiamondCut.sol, 有如下的数据结构:
struct FacetCut{
address facetAddress;// 当前钻石面(Facet)的地址
FacetCutAction action;// 当前DiamondCut的操作,增删改查
bytes4[] functionSelectors;// 该钻石面(Facet)所支持的函数选择子的集合
}
在IFacet.sol, 有如下的数据结构:
structFacet{
address facetAddress;// 本钻石面(Facet)地址
bytes4[] functionSelectors;// 本钻石面(Facet)所支持的函数选择子的集合
}
IDiamondLoupe.sol中定义了需要实现的4个函数:
/// @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_); //返回指定函数选择子所属的钻石面的地址
在最重要LibDiamond.sol中,定义了下面的数据结构
struct FacetAddressAndSelectorPosition{
address facetAddress;
uint16 selectorPosition;
}
// 每个钻石合约及其各个钻石面合约共享的全局变量
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 contractaddress contractOwner;
}
4.2 代码分析
4.2.1 钻石库合约 - LibDiamond.sol
本文件是钻石标准实现中最重要的部分
addReplaceRemoveFacetSelectors(uint256 _selectorCount, address _newFacetAddress, IDiamondCut.FacetCutAction _action, bytes4[] memory _selectors ) internal returns(uint256)
功能: 这个函数是用来增/替换/删除指定Facet的函数选择子
逻辑:
第一种情况 如果newFacetAddress不为0, 则action是增加或者替换操作
遍历传入参数_selectors
对于遍历的每一个函数选择子,找到其所在的Facet
如果是增加操作,上面Step2里的Facet必须存在。
调整全局变量dsStroage里的映射和数组
// 更新相应函数选择子对应的Facet地址和ds.facetAddressAndSelectorPosition[selector] = FacetAddressAndSelectorPosition( _newFacetAddress,uint16(_selectorCount) ); ds.selectors.push(selector);// 把新的函数选择子推入全局数组
如果是替换操作,则用新Facet的地址替换dsStorage里的相应函数选择子对应的Facet
ds.facetAddressAndSelectorPosition[selector].facetAddress = _newFacetAddress;
第二种情况 如果newFacetAddress为0, 则action是删除操作
遍历输入参数_selectors
对于每个函数选择子,找到其现在的Facet地址和Selector的数组位置
如果该FacetAddressAndSelectorPosition中selector的位置不是最后一个,则和最后一个函数选择子交换;否则直接删除
4.2.2 包装合约 - Diamond.sol
Dianmond合约是标准的入口,其实就是一个代理合约。Diamond合约中除了一些初始化的工作以外,最主要的下面的Proxy功能。
下面的程序要点:
ds.slot是内联汇编,意思是获取状态变量ds的Slot(存储槽)位置。见https://solidity.readthedocs.io/en/v0.7.4/assembly.html
通过调用的传递过来的函数选择子获取facet的地址
通过Delegatecall汇编指令来调用相应钻石面(facet)里的函数
fallback() external payable {
LibDiamond.DiamondStoragestorage ds;
bytes32position = LibDiamond.DIAMOND_STORAGE_POSITION;
assembly{ds.slot:= position}
addressfacet = address(bytes20(ds.facetAddressAndSelectorPosition[msg.sig].facetAddress));
require(facet!= address(0), "Diamond: Function does not exist");
assembly{
calldatacopy(0,0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0,0, returndatasize())
switch result
case0 {revert(0,returndatasize())}
default{return(0,returndatasize())}
}
}
5. 如何升级合约
5.1 合约代码升级
代码的升级很容易理解,Diamond.sol里的代理功能(通过Fallback函数实现),会根据函数选择子将函数调用转到相应的Facet。参见本文4.2.2
5.2 合约存储升级
如下图所示,所有的Diamond合约存储必须定义在Diamond中。(DiamondStorage1,DiamondStorage2,DiamondStorage3都定义在Diamond合约里)
而这会带来一个问题,如果我升级一个Facet,要新增一个State Variable,怎么办?
Diamond合约提出了两种方法:
5.2.1 New Storage Layout
参照文献1,可以看到,定义了一个全局的MyStorage, 在其中定义了一个数据哈希到其值的映射
contract MyStorageContract {
// The state variables we care about.
struct MyStorage {
uint aVar;
bytes myBytes;
mapping(uint=>bytes32) myMap;
}
// Creates and returns the storage pointer to the struct.
function myStorage()internalpurereturns(MyStorage storage ms){
// ms_slot = keccak256("com.mycompany.my.storage")
assembly {ms_slot:=0xabcd55b489adb030b...d09c4154cf0}
}
}
contract MyContract is MyStorageContract {
functiondoSomething(uint selector, bytes32 myData)external{
MyStorage storage ms = myStorage();
ms.myMap[selector] = myData;
ms.aVar = uint(myData);
//... more code;
}
function returnMyData() external view returns(bytes32){
MyStorage storage ms = myStorage();
bytes32 data = ms.myMap[ms.aVar];
//... more codereturndata;
}
}
5.2.2 让Diamond合约可升级
如上面的名字所说:因为所有合约存储在Diamond合约里,升级Diamond使其包含新增的State Variable. 同时在升级后的合约里调用registerFacets()重新注册老Diamond合约里的Facets.
实现请参照文献6和文献9
6. 参考文献
https://medium.com/1milliondevs/new-storage-layout-for-proxy-contracts-and-diamonds-98d01d0eadb
https://dev.to/mudgen/understanding-diamonds-on-ethereum-1fb
https://learnblockchain.cn/article/1398
https://medium.com/1milliondevs/solidity-storage-layout-for-proxy-contracts-and-diamonds-c4f009b6903
https://hiddentao.com/archives/2020/05/28/upgradeable-smart-contracts-using-diamond-standard
https://hiddentao.com/archives/2019/10/03/upgradeable-smart-contracts-with-eternal-storage
https://hiddentao.com/archives/2020/03/19/nested-delegate-call-in-solidity
https://dev.to/mudgen/how-diamond-storage-works-90e
https://medium.com/coinmonks/smart-contracts-sharing-common-data-777310263ac0
https://medium.com/coinmonks/sharing-common-data-using-libraries-6573857d328c