摘要:接上篇合約升級模式介紹筆者改寫了一個可用于實踐生產的升級框架,需要自取。在介紹合約升級模式中提到了一個可以解決這個問題的方法。深度理解注意為中的低階方法下文中出現的方法,是我在智能合約中寫的一個方法名稱,不要混淆。
接上篇:合約升級模式介紹智能合約升級的目的筆者改寫了一個可用于實踐生產的升級框架,需要自取。https://github.com/hammewang/...
同時歡迎討論,微信xiuxiu1998
鑒于以太坊智能合約一旦部署,無法修改的原則,所以智能合約升級應當遵循如下兩點規則:
邏輯可升級;
存儲可繼承;
第一點很好理解,可以把代理合約和邏輯合約看成插座和插頭的關系,需要升級的時候把老的插頭拔下,再插上新的即可。
對于第二點,存儲可繼承,不僅僅是存儲結構的繼承,而且在存儲內容上,實現擴展:舊存儲內容不變,新存儲內容繼續追加。這個過程類似于城市化的推進,城市的邊緣可以一圈一圈擴大,但是如果要尋址到老城區的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中的低階方法;
下文中出現的delegateCall方法,是我在智能合約中寫的一個方法名稱,不要混淆。
delegatecall的目的是可以維持執行環境中上下文的一致性,一種很典型的應用場景就是調用library中的方法,用的就是delegatecall。下面來具體介紹一下delegatecall的特點。
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; } }
這時分別部署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——"無中生有"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方法并不是按照變量名稱操作的,而是按照變量所對應的存儲槽的位置,對該位置中的值進行操作。因此,我們是不是事先在代理合約中聲明了變量,就并不重要了。
delegatecall總結可以傳遞msg.sender
不按照變量名進行操作,而是去找變量對應的存儲槽進行操作(無論變量是否在代理合約中事先聲明)
正因為第二點特性,為合約升級中的存儲擴展提供了可能性;同時,也提出了一個很嚴格的要求:
新合約和舊合約之間必須嚴格遵守繼承的模式,即:
contract newLogic is previousVersionLogic{ ... }使用存儲繼承模式升級 原理介紹
------- ========================= | Proxy | ║ UpgradeabilityStorage ║ ------- ========================= ↑ ↑ ↑ --------------------- ------------- | UpgradeabilityProxy | | Upgradeable | --------------------- ------------- ↑ ↑ ---------- ---------- | Token_V0 | ← | Token_V1 | ---------- ----------
代理合約是UpgradeabilityProxy實例,圖中的Token_V0和Token_V1即是邏輯合約的最初版和升級版,它們都必須繼承Upgradeable,同時邏輯合約和代理合約都必須繼承UpgradeabilityStorage,繼承同一套存儲結構,以保證邏輯合約在代理合約中執行時,不會出現變量覆蓋的情況。
具體代碼結構注:圖中每個方框的結構從上到下依次是:合約名稱、狀態變量、function、event、modifier
圖中可以更加清晰地看到,代理合約和邏輯合約都必須繼承registry和_implementation兩個狀態變量,并且邏輯合約中沒有修改前兩個狀態變量的相應方法,因此代理合約中的存儲安全。
升級操作 1. 如何初始化部署Registry合約
部署邏輯合約的初始版本(V1),并確保它繼承了Upgradeable合約
向Registry合約中注冊這個最初版本(V1)的地址
要求Registry合約創建一個UpgradeabilityProxy實例
調用你的UpgrageabilityProxy實例來升級到你最初版本(V1)
2. 如何升級部署一個繼承了你最初版本合約的新版本(V2),V2必須繼承V1
向Registry中注冊合約的新版本V2
調用你的UpgradeabilityProxy實例來升級到最新注冊的版本
3. 如何轉移proxy合約所有權調用Registry中的transferProxyOwnership方法進行所有權轉移;
代碼調用注意事項須對代理合約的地址套用當前版本的邏輯合約的ABI,方能正常調用和獲取返回值。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/24162.html
摘要:以后的邏輯合約可以升級現有的方法或者創造新的方法,但是不能引入新的狀態變量。使用非結構化存儲升級非結構化存儲模式和繼承存儲類似,但是不要求邏輯合約繼承任何和升級相關的狀態變量。 以太坊最大的優勢就是,每一筆用來轉賬、部署合約或者和合約交互的交易(事務)都被存在一個叫做區塊鏈的公共賬本上。一旦交易發生,就再也無法隱藏或者改變。這帶來一個巨大的好處,就是在以太坊中的每一個節點都可以去驗證任...
摘要:以太坊開發高級語言學習。地址以太坊區塊鏈由賬戶組成,你可以把它想象成銀行賬戶。使用很安全,因為它具有以太坊區塊鏈的安全保障除非竊取與以太坊地址相關聯的私鑰,否則是沒有辦法修改其他人的數據的。 以太坊開發高級語言學習。 一、映射(Mapping)和地址(Address) 我們通過給數據庫中的僵尸指定主人, 來支持多玩家模式。 如此一來,我們需要引入2個新的數據類型:mapping(映射)...
摘要:狀態變量合約內聲明的公有變量還有一個存儲位置是,用來存儲函數參數,是只讀的,不會永久存儲的一個數據位置。稱這個為狀態改變,這也是合約級變量稱為狀態變量的原因。 本文首發于深入淺出區塊鏈社區原文鏈接:智能合約語言 Solidity 教程系列4 - 數據存儲位置分析原文已更新,請讀者前往原文閱讀 Solidity教程系列第4篇 - Solidity數據位置分析。 寫在前面 Solidity...
摘要:另外只能做狀態變量,不能做本地局部變量。語法聲明映射類型包括類型包括狀態變量報錯。可視度指的是,決定函數或者狀態變量的可以被哪些智能合約可見和調用。狀態變量可見性沒有。在中,通過來抽象出狀態變量自增的代碼,并修飾。 Mapping 映射(Mappings):類似于哈希表。mapping 中任何一個可能的 key 都對應著一個 value,它的默認值是default-value 。底層用...
閱讀 3891·2021-11-22 13:54
閱讀 2669·2021-09-30 09:48
閱讀 2353·2021-09-28 09:36
閱讀 3104·2021-09-22 15:26
閱讀 1334·2019-08-30 15:55
閱讀 2505·2019-08-30 15:54
閱讀 1419·2019-08-30 14:17
閱讀 2335·2019-08-28 18:25