接上篇:合約升級模式介紹筆者改寫了一個可用於實踐生產的升級框架,須要自取。https://github.com/hammewang/...git
同時歡迎討論,微信xiuxiu1998github
鑑於以太坊智能合約一旦部署,沒法修改的原則,因此智能合約升級應當遵循以下兩點規則:安全
第一點很好理解,能夠把代理合約和邏輯合約當作插座和插頭的關係,須要升級的時候把老的插頭拔下,再插上新的便可。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爲assembly中的低階方法;ui
下文中出現的delegateCall方法,是我在智能合約中寫的一個方法名稱,不要混淆。this
delegatecall的目的是能夠維持執行環境中上下文的一致性,一種很典型的應用場景就是調用library中的方法,用的就是delegatecall。下面來具體介紹一下delegatecall的特色。spa
假設personA調用了contractA中的functionA,這個方法內部同時使用了delegatecall調用了contractB中的functionB,那麼對於functionB來講,msg.sender依然是personA,而不是contractA.
請看下面的合約:
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; } }
這時分別部署proxy
和logic
,以後把logic.address
賦值給proxy
中的logicAddress
變量。調用getPositionAt(0)
會發現返回的也是logicAddress
的值,結果以下圖:
這時,若是調用proxy
中的delegateCall
並傳入0x9137c1a7000000000000000000000000bcb9c87f53878af6dd7a8baf1b24bab6a62fe7aa
(9137c1a7
是setStorage
的方法簽名),意爲用delegatecall
調用logic
中的setStorage方法
,這時會發現proxy
中的logicAddress
發生了變化,變成了咱們剛剛傳入的值。以下:
這時咱們會發現,delegatecall
並不經過變量名稱來修改變量值,而是修改變量所在的存儲槽。因此當在proxy
中delegatecallsetStorage
方法時,修改的並非address a
,而是address a
所在的第0個存儲槽的值,而proxy
中第0個存儲槽存放的是logicAddress
,因此相應就會被覆蓋。
理解到這一步,就能夠感覺到delegatecall的強大和危險。但同時也帶來一層疑問:雖然使用delegatecall可使用邏輯合約中的方法改變代理合約中相應位置的變量,可是並無起到存儲可擴展呀?不還得事先在代理合約中建立相應變量麼?這就至關於在1949年新中國創建的時候,就要規劃之後建設的全部佈局,包括共享單車停靠點,這不是有點扯淡麼?
這就要說到delegatecall下面一個特色了。
delegatecall還有一個強大的特色就是,能夠爲proxy中未事先聲明的變量開闢存儲空間。
咱們來看下一個例子,代理合約依然使用上面用過的proxy
,咱們把邏輯合約 變一下:
contract logic2 { address public a; address public b; function setStorageB(address _a) public { b = _a; } }
新增長一個address變量,而且只修改第二個address變量。
這時依然重複上一個例子的第一步,把logic2
的地址賦值給代理合約中的logicAddress
變量。結果以下圖:
而後使用代理合約中的detegateCall
方法,調用logic2
中的setStorage2
方法,傳入data
爲0x9ea338be0000000000000000000000000dcd2f752394c41875e259e00bb44fd505297caf
。以後再調用getPositionAt(1)
和logicAddress()
方法,結果以下圖:
能夠看到logicAddress
並無發生變化,而第1個存儲槽中的值變成了咱們剛剛傳入的值。
這也再次說明了,delegatecall
方法並非按照變量名稱操做的,而是按照變量所對應的存儲槽的位置,對該位置中的值進行操做。所以,咱們是否是事先在代理合約中聲明瞭變量,就並不重要了。
正由於第二點特性,爲合約升級中的存儲擴展提供了可能性;同時,也提出了一個很嚴格的要求:
新合約和舊合約之間必須嚴格遵照繼承的模式,即:
contract newLogic is previousVersionLogic{ ... }
------- ========================= | Proxy | ║ UpgradeabilityStorage ║ ------- ========================= ↑ ↑ ↑ --------------------- ------------- | UpgradeabilityProxy | | Upgradeable | --------------------- ------------- ↑ ↑ ---------- ---------- | Token_V0 | ← | Token_V1 | ---------- ----------
代理合約是UpgradeabilityProxy
實例,圖中的Token_V0
和Token_V1
便是邏輯合約的最第一版和升級版,它們都必須繼承Upgradeable
,同時邏輯合約和代理合約都必須繼承UpgradeabilityStorage
,繼承同一套存儲結構,以保證邏輯合約在代理合約中執行時,不會出現變量覆蓋的狀況。
注:圖中每一個方框的結構從上到下依次是:合約名稱、狀態變量、function、event、modifier
圖中能夠更加清晰地看到,代理合約和邏輯合約都必須繼承registry
和_implementation
兩個狀態變量,而且邏輯合約中沒有修改前兩個狀態變量的相應方法,所以代理合約中的存儲安全。
Registry
合約Upgradeable
合約Registry
合約中註冊這個最第一版本(V1)的地址Registry
合約建立一個UpgradeabilityProxy
實例UpgrageabilityProxy
實例來升級到你最第一版本(V1)Registry
中註冊合約的新版本V2UpgradeabilityProxy
實例來升級到最新註冊的版本調用Registry
中的transferProxyOwnership
方法進行全部權轉移;
須對代理合約的地址套用當前版本的邏輯合約的ABI,方能正常調用和獲取返回值。