深度剖析智能合約升級(jí)——inherited storage

接上篇:合約升級(jí)模式介紹

筆者改寫了一個(gè)可用于實(shí)踐生產(chǎn)的升級(jí)框架,需要自取。https://github.com/hammewang/Proxy

同時(shí)歡迎討論,微信xiuxiu1998

智能合約升級(jí)的目的

鑒于以太坊智能合約一旦部署,無法修改的原則,所以智能合約升級(jí)應(yīng)當(dāng)遵循如下兩點(diǎn)規(guī)則:

  1. 邏輯可升級(jí);
  2. 存儲(chǔ)可繼承;

第一點(diǎn)很好理解,可以把代理合約和邏輯合約看成插座和插頭的關(guān)系,需要升級(jí)的時(shí)候把老的插頭拔下,再插上新的即可。

對(duì)于第二點(diǎn),存儲(chǔ)可繼承,不僅僅是存儲(chǔ)結(jié)構(gòu)的繼承,而且在存儲(chǔ)內(nèi)容上,實(shí)現(xiàn)擴(kuò)展:舊存儲(chǔ)內(nèi)容不變,新存儲(chǔ)內(nèi)容繼續(xù)追加。這個(gè)過程類似于城市化的推進(jìn),城市的邊緣可以一圈一圈擴(kuò)大,但是如果要尋址到老城區(qū)的XX路XX號(hào),無論城市怎么擴(kuò)大,拿著這個(gè)門牌號(hào)依然可以找到那棟老建筑。

升級(jí)方式

升級(jí)目的中的第一點(diǎn)是相對(duì)好實(shí)現(xiàn)的,只要改變調(diào)用的邏輯合約地址就可以了;而為了實(shí)現(xiàn)第二點(diǎn),就要保證合約執(zhí)行環(huán)境上下文保持一致。在介紹合約升級(jí)模式中提到了一個(gè)可以解決這個(gè)問題的方法:delegatecall。把關(guān)鍵代碼再貼一遍:

 assembly {
    // 獲得自由內(nèi)存指針
    let ptr := mload(0x40)
    // 復(fù)制calldata到內(nèi)存中
    calldatacopy(ptr, 0, calldatasize) 
    // 使用delegatecall處理calldata
    let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
    // 返回值大小
    let size := returndatasize
    // 把返回值復(fù)制到內(nèi)存中
    returndatacopy(ptr, 0, size)

    switch result
    case 0 { revert(ptr, size) } // 執(zhí)行失敗
    default { return(ptr, size) } // 執(zhí)行成功,返回內(nèi)存中的返回值
 }

這樣做,實(shí)現(xiàn)了把邏輯合約(_impl)中的方法拉到代理合約中執(zhí)行,遵循代理合約的上下文(如存儲(chǔ)、余額等),通過這種方式實(shí)現(xiàn)了執(zhí)行上下文一致性。

深度理解delegatecall

注意:

delegatecall為assembly中的低階方法;

下文中出現(xiàn)的delegateCall方法,是我在智能合約中寫的一個(gè)方法名稱,不要混淆。

delegatecall的目的是可以維持執(zhí)行環(huán)境中上下文的一致性,一種很典型的應(yīng)用場(chǎng)景就是調(diào)用library中的方法,用的就是delegatecall。下面來具體介紹一下delegatecall的特點(diǎn)。

1. 可以傳遞msg.sender

假設(shè)personA調(diào)用了contractA中的functionA,這個(gè)方法內(nèi)部同時(shí)使用了delegatecall調(diào)用了contractB中的functionB,那么對(duì)于functionB來說,msg.sender依然是personA,而不是contractA.

2.可以改變同一存儲(chǔ)槽中的內(nèi)容

請(qǐng)看下面的合約:

pragma solidity ^0.4.24;

contract proxy {
    address public logicAddress;
    
    function setLogic(address _a) public {
        logicAddress = _a;
    }
    
    function delegateCall(bytes data) public {
        this.call.value(msg.value)(data);
    }
    
    function () payable public {
    address _impl = logicAddress;
    require(_impl != address(0));

    assembly {
      let ptr := mload(0x40) 
      calldatacopy(ptr, 0, calldatasize) 
      let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
      let size := returndatasize 
      returndatacopy(ptr, 0, size) 

      switch result
      case 0 { revert(ptr, size) }
      default { return(ptr, size) }
    }
  }
    
    function getPositionAt(uint n) public view returns (address) {
        assembly {
            let d := sload(n)
            mstore(0x80, d)
            return(0x80,32)
      }
    }
}

contract logic {
    address public a;
     function setStorage(address _a) public {
         a = _a;
     }
}

這時(shí)分別部署proxylogic,之后把logic.address賦值給proxy中的logicAddress變量。調(diào)用getPositionAt(0)會(huì)發(fā)現(xiàn)返回的也是logicAddress的值,結(jié)果如下圖:

delegatecall_changeStorageSlot

這時(shí),如果調(diào)用proxy中的delegateCall并傳入0x9137c1a7000000000000000000000000bcb9c87f53878af6dd7a8baf1b24bab6a62fe7aa9137c1a7setStorage的方法簽名),意為用delegatecall調(diào)用logic中的setStorage方法,這時(shí)會(huì)發(fā)現(xiàn)proxy中的logicAddress發(fā)生了變化,變成了我們剛剛傳入的值。如下:

delegatecall_changeStorageSlot

這時(shí)我們會(huì)發(fā)現(xiàn),delegatecall并不通過變量名稱來修改變量值,而是修改變量所在的存儲(chǔ)槽。所以當(dāng)在proxy中delegatecallsetStorage方法時(shí),修改的并不是address a,而是address a所在的第0個(gè)存儲(chǔ)槽的值,而proxy中第0個(gè)存儲(chǔ)槽存放的是logicAddress,所以相應(yīng)就會(huì)被覆蓋。

理解到這一步,就可以感受到delegatecall的強(qiáng)大和危險(xiǎn)。但同時(shí)也帶來一層疑問:雖然使用delegatecall可以使用邏輯合約中的方法改變代理合約中相應(yīng)位置的變量,但是并沒有起到存儲(chǔ)可擴(kuò)展呀?不還得事先在代理合約中創(chuàng)建相應(yīng)變量么?這就相當(dāng)于在1949年新中國建立的時(shí)候,就要規(guī)劃以后建設(shè)的所有布局,包括共享單車??奎c(diǎn),這不是有點(diǎn)扯淡么?

這就要說到delegatecall下面一個(gè)特點(diǎn)了。

delegatecall——"無中生有"

delegatecall還有一個(gè)強(qiáng)大的特點(diǎn)就是,可以為proxy中未事先聲明的變量開辟存儲(chǔ)空間。

我們來看下一個(gè)例子,代理合約依然使用上面用過的proxy,我們把邏輯合約 變一下:

contract logic2 {
    address public a;
    address public b;
     function setStorageB(address _a) public {
         b = _a;
     }
}

新增加一個(gè)address變量,并且只修改第二個(gè)address變量。

這時(shí)依然重復(fù)上一個(gè)例子的第一步,把logic2的地址賦值給代理合約中的logicAddress變量。結(jié)果如下圖:

delegatecall_changeStorageSlot

然后使用代理合約中的detegateCall方法,調(diào)用logic2中的setStorage2方法,傳入data0x9ea338be0000000000000000000000000dcd2f752394c41875e259e00bb44fd505297caf。之后再調(diào)用getPositionAt(1)logicAddress()方法,結(jié)果如下圖:

delegatecall_changeStorageSlot

可以看到logicAddress并沒有發(fā)生變化,而第1個(gè)存儲(chǔ)槽中的值變成了我們剛剛傳入的值。

這也再次說明了,delegatecall方法并不是按照變量名稱操作的,而是按照變量所對(duì)應(yīng)的存儲(chǔ)槽的位置,對(duì)該位置中的值進(jìn)行操作。因此,我們是不是事先在代理合約中聲明了變量,就并不重要了。

delegatecall總結(jié)

  1. 可以傳遞msg.sender
  2. 不按照變量名進(jìn)行操作,而是去找變量對(duì)應(yīng)的存儲(chǔ)槽進(jìn)行操作(無論變量是否在代理合約中事先聲明)

正因?yàn)榈诙c(diǎn)特性,為合約升級(jí)中的存儲(chǔ)擴(kuò)展提供了可能性;同時(shí),也提出了一個(gè)很嚴(yán)格的要求:

新合約和舊合約之間必須嚴(yán)格遵守繼承的模式,即:

contract newLogic is previousVersionLogic{
    ...
}

使用存儲(chǔ)繼承模式升級(jí)

原理介紹

               -------             =========================
              | Proxy |           ║  UpgradeabilityStorage  ║
               -------             =========================
                  ↑                 ↑                     ↑            
                 ---------------------              -------------
                | UpgradeabilityProxy |            | Upgradeable |
                 ---------------------              ------------- 
                                                      ↑        ↑
                                              ----------      ---------- 
                                             | Token_V0 |  ← | Token_V1 |         
                                              ----------      ---------- 

代理合約是UpgradeabilityProxy實(shí)例,圖中的Token_V0Token_V1即是邏輯合約的最初版和升級(jí)版,它們都必須繼承Upgradeable,同時(shí)邏輯合約和代理合約都必須繼承UpgradeabilityStorage,繼承同一套存儲(chǔ)結(jié)構(gòu),以保證邏輯合約在代理合約中執(zhí)行時(shí),不會(huì)出現(xiàn)變量覆蓋的情況。

具體代碼結(jié)構(gòu)

upgradable_using_inherited_storage

注:圖中每個(gè)方框的結(jié)構(gòu)從上到下依次是:合約名稱、狀態(tài)變量、function、event、modifier

圖中可以更加清晰地看到,代理合約和邏輯合約都必須繼承registry_implementation兩個(gè)狀態(tài)變量,并且邏輯合約中沒有修改前兩個(gè)狀態(tài)變量的相應(yīng)方法,因此代理合約中的存儲(chǔ)安全。

升級(jí)操作

1. 如何初始化

  1. 部署Registry合約
  2. 部署邏輯合約的初始版本(V1),并確保它繼承了Upgradeable合約
  3. Registry合約中注冊(cè)這個(gè)最初版本(V1)的地址
  4. 要求Registry合約創(chuàng)建一個(gè)UpgradeabilityProxy實(shí)例
  5. 調(diào)用你的UpgrageabilityProxy實(shí)例來升級(jí)到你最初版本(V1)

2. 如何升級(jí)

  1. 部署一個(gè)繼承了你最初版本合約的新版本(V2),V2必須繼承V1
  2. Registry中注冊(cè)合約的新版本V2
  3. 調(diào)用你的UpgradeabilityProxy實(shí)例來升級(jí)到最新注冊(cè)的版本

3. 如何轉(zhuǎn)移proxy合約所有權(quán)

調(diào)用Registry中的transferProxyOwnership方法進(jìn)行所有權(quán)轉(zhuǎn)移;

代碼調(diào)用注意事項(xiàng)

須對(duì)代理合約的地址套用當(dāng)前版本的邏輯合約的ABI,方能正常調(diào)用和獲取返回值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 以太坊最大的優(yōu)勢(shì)就是,每一筆用來轉(zhuǎn)賬、部署合約或者和合約交互的交易(事務(wù))都被存在一個(gè)叫做區(qū)塊鏈的公共賬本上。一旦...
    王鐵塔閱讀 1,563評(píng)論 0 0
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,648評(píng)論 19 139
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,674評(píng)論 1 32
  • 有人說:好孩子都是夸出來的。你也這么認(rèn)為嗎?來看看下面一個(gè)故事: 對(duì)話劇和音樂劇有興趣的讀者大概都聽說過托尼獎(jiǎng)吧。...
    玲玲A閱讀 274評(píng)論 0 2
  • 文/匿名用戶 應(yīng)該從哪說起呢,腦子里關(guān)于他們的信息一多,反而理不清了。 FS 對(duì) 360安全衛(wèi)士的貢獻(xiàn)自然毋庸置疑...
    耕心鮮閱讀 1,852評(píng)論 0 3

友情鏈接更多精彩內(nèi)容