深度剖析智能合約升級——inherited storage

接上篇:合約升級模式介紹

筆者改寫了一個可用於實踐生產的升級框架,須要自取。https://github.com/hammewang/...git

同時歡迎討論,微信xiuxiu1998github

智能合約升級的目的

鑑於以太坊智能合約一旦部署,沒法修改的原則,因此智能合約升級應當遵循以下兩點規則:安全

  1. 邏輯可升級;
  2. 存儲可繼承;

第一點很好理解,能夠把代理合約和邏輯合約當作插座和插頭的關係,須要升級的時候把老的插頭拔下,再插上新的便可。bash

對於第二點,存儲可繼承,不只僅是存儲結構的繼承,並且在存儲內容上,實現擴展:舊存儲內容不變,新存儲內容繼續追加。這個過程相似於城市化的推動,城市的邊緣能夠一圈一圈擴大,可是若是要尋址到老城區的XX路XX號,不管城市怎麼擴大,拿着這個門牌號依然能夠找到那棟老建築。微信

升級方式

升級目的中的第一點是相對好實現的,只要改變調用的邏輯合約地址就能夠了;而爲了實現第二點,就要保證合約執行環境上下文保持一致。在介紹合約升級模式中提到了一個能夠解決這個問題的方法:delegatecall。把關鍵代碼再貼一遍:框架

assembly {
    // 得到自由內存指針
    let ptr := mload(0x40)
    // 複製calldata到內存中
    calldatacopy(ptr, 0, calldatasize) 
    // 使用delegatecall處理calldata
    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) } // 執行成功,返回內存中的返回值
 }

這樣作,實現了把邏輯合約(_impl)中的方法拉到代理合約中執行,遵循代理合約的上下文(如存儲、餘額等),經過這種方式實現了執行上下文一致性。佈局

深度理解delegatecall

注意:

delegatecall爲assembly中的低階方法;ui

下文中出現的delegateCall方法,是我在智能合約中寫的一個方法名稱,不要混淆。this

delegatecall的目的是能夠維持執行環境中上下文的一致性,一種很典型的應用場景就是調用library中的方法,用的就是delegatecall。下面來具體介紹一下delegatecall的特色。spa

1. 能夠傳遞msg.sender

假設personA調用了contractA中的functionA,這個方法內部同時使用了delegatecall調用了contractB中的functionB,那麼對於functionB來講,msg.sender依然是personA,而不是contractA.

2.能夠改變同一存儲槽中的內容

請看下面的合約:

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;
     }
}

這時分別部署proxylogic,以後把logic.address賦值給proxy中的logicAddress變量。調用getPositionAt(0)會發現返回的也是logicAddress的值,結果以下圖:

delegatecall_changeStorageSlot

這時,若是調用proxy中的delegateCall並傳入0x9137c1a7000000000000000000000000bcb9c87f53878af6dd7a8baf1b24bab6a62fe7aa9137c1a7setStorage的方法簽名),意爲用delegatecall調用logic中的setStorage方法,這時會發現proxy中的logicAddress發生了變化,變成了咱們剛剛傳入的值。以下:

delegatecall_changeStorageSlot

這時咱們會發現,delegatecall並不經過變量名稱來修改變量值,而是修改變量所在的存儲槽。因此當在proxy中delegatecallsetStorage方法時,修改的並非address a,而是address a所在的第0個存儲槽的值,而proxy中第0個存儲槽存放的是logicAddress,因此相應就會被覆蓋。

理解到這一步,就能夠感覺到delegatecall的強大和危險。但同時也帶來一層疑問:雖然使用delegatecall可使用邏輯合約中的方法改變代理合約中相應位置的變量,可是並無起到存儲可擴展呀?不還得事先在代理合約中建立相應變量麼?這就至關於在1949年新中國創建的時候,就要規劃之後建設的全部佈局,包括共享單車停靠點,這不是有點扯淡麼?

這就要說到delegatecall下面一個特色了。

delegatecall——"無中生有"

delegatecall還有一個強大的特色就是,能夠爲proxy中未事先聲明的變量開闢存儲空間

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

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

新增長一個address變量,而且只修改第二個address變量。

這時依然重複上一個例子的第一步,把logic2的地址賦值給代理合約中的logicAddress變量。結果以下圖:

delegatecall_changeStorageSlot

而後使用代理合約中的detegateCall方法,調用logic2中的setStorage2方法,傳入data0x9ea338be0000000000000000000000000dcd2f752394c41875e259e00bb44fd505297caf。以後再調用getPositionAt(1)logicAddress()方法,結果以下圖:

delegatecall_changeStorageSlot

能夠看到logicAddress並無發生變化,而第1個存儲槽中的值變成了咱們剛剛傳入的值。

這也再次說明了,delegatecall方法並非按照變量名稱操做的,而是按照變量所對應的存儲槽的位置,對該位置中的值進行操做。所以,咱們是否是事先在代理合約中聲明瞭變量,就並不重要了。

delegatecall總結

  1. 能夠傳遞msg.sender
  2. 不按照變量名進行操做,而是去找變量對應的存儲槽進行操做(不管變量是否在代理合約中事先聲明)

正由於第二點特性,爲合約升級中的存儲擴展提供了可能性;同時,也提出了一個很嚴格的要求:

新合約和舊合約之間必須嚴格遵照繼承的模式,即:

contract newLogic is previousVersionLogic{
    ...
}

使用存儲繼承模式升級

原理介紹

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

代理合約是UpgradeabilityProxy實例,圖中的Token_V0Token_V1便是邏輯合約的最第一版和升級版,它們都必須繼承Upgradeable,同時邏輯合約和代理合約都必須繼承UpgradeabilityStorage,繼承同一套存儲結構,以保證邏輯合約在代理合約中執行時,不會出現變量覆蓋的狀況。

具體代碼結構

upgradable_using_inherited_storage

注:圖中每一個方框的結構從上到下依次是:合約名稱、狀態變量、function、event、modifier

圖中能夠更加清晰地看到,代理合約和邏輯合約都必須繼承registry_implementation兩個狀態變量,而且邏輯合約中沒有修改前兩個狀態變量的相應方法,所以代理合約中的存儲安全。

升級操做

1. 如何初始化

  1. 部署Registry合約
  2. 部署邏輯合約的初始版本(V1),並確保它繼承了Upgradeable合約
  3. Registry合約中註冊這個最第一版本(V1)的地址
  4. 要求Registry合約建立一個UpgradeabilityProxy實例
  5. 調用你的UpgrageabilityProxy實例來升級到你最第一版本(V1)

2. 如何升級

  1. 部署一個繼承了你最第一版本合約的新版本(V2),V2必須繼承V1
  2. Registry中註冊合約的新版本V2
  3. 調用你的UpgradeabilityProxy實例來升級到最新註冊的版本

3. 如何轉移proxy合約全部權

調用Registry中的transferProxyOwnership方法進行全部權轉移;

代碼調用注意事項

須對代理合約的地址套用當前版本的邏輯合約的ABI,方能正常調用和獲取返回值。

相關文章
相關標籤/搜索