钻石标准--Diamond Standard

钻石标准主要是为了对应以太坊上智能合约大小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://naturaldao.io/collaboration/blog-cn/113-eip-2535%E5%8D%B3%E9%92%BB%E7%9F%B3%E6%A0%87%E5%87%86%E4%BB%8B%E7%BB%8D.html

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://github.com/nayms

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容