以太坊最大的優(yōu)勢就是烟馅,每一筆用來轉(zhuǎn)賬涛浙、部署合約或者和合約交互的交易(事務(wù))都被存在一個(gè)叫做區(qū)塊鏈的公共賬本上康辑。一旦交易發(fā)生,就再也無法隱藏或者改變轿亮。這帶來一個(gè)巨大的好處疮薇,就是在以太坊中的每一個(gè)節(jié)點(diǎn)都可以去驗(yàn)證任意一筆交易的合法性和當(dāng)前狀態(tài)。這使得以太坊成為一個(gè)非常健壯的去中心化系統(tǒng)我注。
但是隨之而來的是按咒,它還有一個(gè)最大的缺點(diǎn),就是智能合約一旦部署之后但骨,就再也無法改變源碼励七。開發(fā)中心化應(yīng)用(比如facebook或者Airbnb)的開發(fā)者,都已經(jīng)習(xí)慣了奔缠,為了修復(fù)bug或者引入新的特性而頻繁更新產(chǎn)品掠抬。但這種方式卻不適用以太坊。
還記得當(dāng)面Parity多簽名錢包被黑導(dǎo)致150000以太幣被偷的惡劣事件嗎校哎?在整個(gè)攻擊中两波,就因?yàn)殄X包中的一個(gè)bug導(dǎo)致很多巨額錢包的資金被清空。而唯一的解決方案就是嘗試以比黑客更快的速度闷哆,利用相同的漏洞攻擊剩余的錢包腰奋,來把以太幣重新分配給它們合法的所有者。
要是有一種方法可以在智能合約部署之后阳准,還能對它們進(jìn)行升級氛堕,那該多好...
引入代理模式
盡管想升級已經(jīng)部署的智能合約中的代碼是不可能的馏臭,但是可以通過設(shè)計(jì)一個(gè)代理合約結(jié)構(gòu)野蝇,這個(gè)結(jié)構(gòu)可以讓你可以通過新部署一個(gè)合約的方式讼稚,來實(shí)現(xiàn)升級主要的處理邏輯的目的。
代理結(jié)構(gòu)模式就像下面這張圖一樣:所有消息通過一個(gè)代理合約來間接調(diào)用最新部署的邏輯合約绕沈。如果想要升級的話锐想,只需要部署一個(gè)新的合約,然后在代理合約中更新引用新的合約地址就可以了乍狐。
作為實(shí)現(xiàn)zeppelin_os的一部分赠摇,zeppelin正致力于實(shí)現(xiàn)集中代理模式。目前已經(jīng)探索出來的有下面三個(gè):
- 繼承存儲模式
Inherited Storage
- 永久存儲模式
Eternal Storage
- 非結(jié)構(gòu)化存儲模式
Unstructured Storage
所有三種模式都依賴低階的delegatecall浅蚪。盡管solidity提供了一個(gè)delegatecall方法藕帜,但它只能返回true或者false來顯示調(diào)用是否成功,而不是允許你操作返回的數(shù)據(jù)惜傲。
在我們深入了解之前洽故,理解兩個(gè)關(guān)鍵的概念很重要:
- 當(dāng)調(diào)用一個(gè)合約中并不支持的的方法時(shí),就會(huì)調(diào)用合約中的fallback方法盗誊。你可以自己寫一個(gè)fallback函數(shù)來處理這種場景时甚。代理合約就是用自定義的fallback方法將調(diào)用重定向到其他合約實(shí)現(xiàn)。
- 每當(dāng)合約A授權(quán)對另一個(gè)合約B的調(diào)用時(shí)哈踱,它就會(huì)在合約A的上下文中執(zhí)行合約B的代碼荒适。這就意味著
msg.value
和msg.sender
的值會(huì)被保留。并且對存儲的修改將會(huì)作用在合約A的存儲上开镣。
zeppelin的代理合約刀诬,為了可以返回調(diào)用邏輯合約后的結(jié)果,實(shí)現(xiàn)了自己的delegatecall方法邪财,所有模式都是這樣舅列。如果你想要使用zeppelin的代理合約代碼,你就要理解代碼的每一個(gè)細(xì)節(jié)卧蜓。讓我們先來看看它是如何發(fā)揮作用的帐要,以及理解為了達(dá)到目的它所使用的assembly操作碼。
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) }
}
為了授權(quán)對另一個(gè)合約中方法的調(diào)用弥奸,我們需要把它賦值給proxy合約接收的msg.data
榨惠。因?yàn)?code>msg.data是bytes
類型的,是一個(gè)動(dòng)態(tài)的數(shù)據(jù)結(jié)構(gòu)盛霎,所以它在msg.data
的第一個(gè)字(word
赠橙,也就是32個(gè)字節(jié))中的存儲長度會(huì)不一樣。如果我們想要只取出真正的數(shù)據(jù)愤炸,我們需要跳過第一個(gè)字(word
)期揪,從msg.data
的0x20
(32個(gè)字節(jié))開始。然而规个,我們會(huì)用到兩個(gè)操作碼來實(shí)現(xiàn)此目的凤薛。我們會(huì)使用calldatasize
來獲取msg.data
的大小姓建,以及calldatacopy
來把它復(fù)制到ptr
所指向的位置。
注意到我們是如何初始化ptr
變量的缤苫。在solidity中速兔,內(nèi)存槽中的0x40
位置是和特殊的,因?yàn)樗鎯α酥赶蛳乱粋€(gè)可用自由內(nèi)存的指針活玲。每次當(dāng)你想往內(nèi)存里存儲一個(gè)變量時(shí)涣狗,你都要檢查存儲在0x40
的值。這就是你變量即將存放的位置∈婧叮現(xiàn)在我們知道了我們要在哪兒存變量镀钓,我們就可以使用calldatacopy
,把大小為calldatasize
的calldata從0開始啊復(fù)制到ptr
指向的那個(gè)位置了镀迂。
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
我們再看看下面的assembly代碼(使用delegatecall
操作碼)
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
解釋一下上面的參數(shù):
-
gas
:函數(shù)執(zhí)行所需的gas -
_impl
:我們調(diào)用的邏輯合約的地址 -
ptr
:內(nèi)存指針(指向數(shù)據(jù)開始存儲的地方) -
calldatasize
:傳入的數(shù)據(jù)大小 -
0
:調(diào)用邏輯合約后的返回值掸宛。我們沒有使用這個(gè)參數(shù)因?yàn)槲覀冞€不知道返回值的大小,所以不能把它賦值給一個(gè)變量招拙。我們可以后面可以進(jìn)一步使用returndata
操作碼來獲取這些信息唧瘾。 -
0
:返回值的大小。這個(gè)參數(shù)也沒有被使用因?yàn)槲覀儧]有機(jī)會(huì)創(chuàng)造一個(gè)臨時(shí)變量用來存儲返回值别凤。鑒于我們在調(diào)用其他合約之前無法知道它的大惺涡颉(所以就無法創(chuàng)造臨時(shí)變量呀)。我們稍后可以用returndatasize
操作碼來得到這個(gè)值规哪。
下面一行代碼就是用returndatasize
操作碼得到了返回?cái)?shù)據(jù)的大星笤ァ:
let size := returndatasize
我們使用這個(gè)返回值的大小,來把返回值復(fù)制到ptr
指向的內(nèi)存诉稍,使用returndatacopy
來達(dá)到這個(gè)目的:
returndatacopy(ptr, 0, size)
最后蝠嘉,switch
語句要么返回 【返回值】,要么拋出錯(cuò)誤杯巨,如果發(fā)生錯(cuò)誤的話蚤告。
很好,我們現(xiàn)在有了一個(gè)從邏輯合約中獲取正確結(jié)果的方法服爷。
現(xiàn)在杜恰,我們理解了代理合約是如何工作的。下面就讓我們正式學(xué)習(xí)三種模式:繼承存儲模式仍源、永久存儲模式和非結(jié)構(gòu)化存儲模式心褐。
這三種方法用不同的方法來解決同一個(gè)難點(diǎn):怎樣確保邏輯合約不會(huì)重寫/覆蓋代理中的狀態(tài)變量。
任何代理結(jié)構(gòu)模式的主要問題就是如何分配存儲笼踩。記住逗爹,既然我們使用一個(gè)合約來存儲,另一個(gè)合約來實(shí)現(xiàn)邏輯嚎于,它們中的任何一個(gè)都有可能重寫一個(gè)已經(jīng)使用的存儲槽掘而。這意味著如果代理合約有一個(gè)狀態(tài)變量在某個(gè)存儲槽中存儲著最新的邏輯合約地址挟冠,但是邏輯合約卻不知道的話,那么邏輯合約可能就會(huì)在那個(gè)槽中存一些其他數(shù)據(jù)镣屹,這樣就把代理合約中的重要信息覆蓋了圃郊。zeppelin的這三種方法代表了架構(gòu)合約系統(tǒng)的三種途徑价涝,實(shí)現(xiàn)通過代理模式升級合約的目的女蜈。
使用繼承存儲模式升級
繼承存儲方法要求邏輯合約內(nèi)部也實(shí)現(xiàn)代理合約內(nèi)的存儲結(jié)構(gòu)。代理合約和邏輯合約都要繼承完全一樣的存儲結(jié)構(gòu)色瘩,來確保二者都支持存儲必要的代理合約的狀態(tài)變量伪窖。
當(dāng)探索這種模式時(shí),我們有這樣一個(gè)想法居兆,我們想要有一個(gè)Registry
合約來追蹤不同版本的邏輯合約覆山。 為了升級成新的邏輯合約,你需要為它在Registry
里注冊一個(gè)新的版本泥栖,并且要求代理合約中也升級成這個(gè)最新版本的邏輯合約簇宽。注意到有一個(gè)Registry
合約并不影響存儲機(jī)制,實(shí)際上吧享,它可以應(yīng)用到這篇文章中提到的任意一種存儲模式中魏割。
如何初始化
- 部署
Registry
合約 - 部署一個(gè)邏輯合約的最初版本(V1),確保它繼承了
Upgradeable
合約 - 向
Registry
合約中注冊這個(gè)最初版本(V1)的地址 - 要求
Registry
合約創(chuàng)建一個(gè)UpgradeabilityProxy
實(shí)例 - 調(diào)用你的
UpgrageabilityProxy
實(shí)例來升級到你最初版本(V1)
如何升級
- 部署一個(gè)繼承了你最初版本合約的新版本(V2)钢颂,==確保它保留了代理合約和最初版本邏輯合約中的存儲結(jié)構(gòu)==
- 向
Registry
中注冊合約的新版本 - 調(diào)用你的
UpgradeabilityProxy
實(shí)例來升級到最新注冊的版本
tips
我們可以在未來部署的邏輯合約中升級現(xiàn)有方法钞它、創(chuàng)造新的方法以及新的狀態(tài)變量,但仍然調(diào)用同一個(gè)UpgradeabilityProxy
合約殊鞭。
使用永久存儲模式升級
在永久存儲模式中遭垛,存儲模式用一個(gè)獨(dú)立的合約(代理和邏輯合約都要繼承這個(gè)合約)來定義。這個(gè)存儲合約保留了所有邏輯合約需要的狀態(tài)變量操灿,因?yàn)榇砗霞s也會(huì)知道這些變量的存在(因?yàn)槔^承)锯仪,它就可以為升級定義自己的狀態(tài)變量,不用考慮覆蓋變量這些問題趾盐。注意到所有的版本的邏輯合約都不可以再定義任何額外的狀態(tài)變量卵酪。所有版本的邏輯合約都必須一直使用一開始就定義好的永久存儲架構(gòu)。
這種應(yīng)用在zeppelin labs項(xiàng)目中提供了實(shí)現(xiàn)谤碳,并且同時(shí)引入了代理所有權(quán)的概念溃卡。一個(gè)代理的所有者是唯一一個(gè)可以升級代理并指定一個(gè)新的邏輯合約的地址,也是唯一一個(gè)可以轉(zhuǎn)移所有權(quán)的地址蜒简。
如何初始化
- 部署一個(gè)
EternalStorageProxy
實(shí)例 - 部署一個(gè)邏輯合約的最初版本(V1)
- 調(diào)用
EternalStorageProxy
實(shí)例來升級到這個(gè)最初版本合約的地址 - 如果你的邏輯合約依賴自己的構(gòu)造函數(shù)(constructor)來設(shè)置某個(gè)初始狀態(tài)瘸羡,那么在它和代理合約產(chǎn)生聯(lián)系之后,之前的這些狀態(tài)就要重新修改搓茬,因?yàn)榇砗霞s的存儲并不知道(邏輯合約里的)這些值犹赖。
EternalStorageProxy
有一個(gè)叫upgradeToAndCall
的函數(shù)專門來調(diào)用一些邏輯合約中的方法队他,一旦代理合約升級到最新版本時(shí),就把連接到的那個(gè)邏輯合約里的初始設(shè)置重新設(shè)置一遍峻村。
如何升級
- 部署一個(gè)邏輯合約的最新版本(v2)麸折,確保它也包含永久存儲結(jié)構(gòu)。
- 調(diào)用
EternalStorageProxy
實(shí)例來升級到最新版本粘昨。
tips
這是沒有增加太多開銷同時(shí)很直觀的邏輯合約垢啼。 以后的邏輯合約可以升級現(xiàn)有的方法或者創(chuàng)造新的方法,但是不能引入新的狀態(tài)變量张肾。
使用非結(jié)構(gòu)化存儲升級
非結(jié)構(gòu)化存儲模式和繼承存儲類似芭析,但是不要求邏輯合約繼承任何和升級相關(guān)的狀態(tài)變量。這個(gè)模式使用代理合約中定義的非結(jié)構(gòu)化的存儲槽來保存升級所需的數(shù)據(jù)吞瞪。
在代理合約中馁启,我們定義了一個(gè)常量,每當(dāng)哈希的時(shí)候芍秆,就給出一個(gè)足夠隨機(jī)的存儲位置來存儲代理合約需要調(diào)用的邏輯合約的地址惯疙。
bytes32 private constant implementationPosition =
keccak256("org.zeppelinos.proxy.implementation");
因?yàn)槌A?恒定)狀態(tài)變量并不占用存儲槽,所以并不用擔(dān)心implementationPosition
會(huì)不小心被邏輯合約占用妖啥。鑒于solidity在存儲中放置狀態(tài)變量的方法霉颠,依然有非常非常非常小的概率可能發(fā)生要存儲新變量的存儲槽已經(jīng)被占用了。
通過使用這種模式迹栓,任何版本的邏輯合約都不需要知道代理合約的存儲結(jié)構(gòu)掉分,但是所有后一個(gè)版本的邏輯合約都必須繼承上一個(gè)版本的存儲變量。就像在繼承存儲模式中一樣,未來的邏輯合約可以更新現(xiàn)有的方法,也可以創(chuàng)建新的方法和新的狀態(tài)變量脯倒。
這個(gè)模式也使用了代理合約所有權(quán)的概念。只有代理合約的所有者可以更新邏輯合約的地址不从,也是唯一可以轉(zhuǎn)移所有權(quán)的地址。
如何初始化
- 部署
OwnedUpgradeabilityProxy
實(shí)例 - 部署邏輯合約的初始版本(V1)
- 調(diào)用
OwnedUpgradeabilityProxy
實(shí)例來更新到初始版本的邏輯合約 - 如果你的邏輯合約依賴自己的構(gòu)造函數(shù)(constructor)來設(shè)置某個(gè)初始狀態(tài)犁跪,那么在它和代理合約產(chǎn)生聯(lián)系之后椿息,之前的這些狀態(tài)就要重新修改,因?yàn)榇砗霞s的存儲并不知道(邏輯合約里的)這些值坷衍。
OwnedUpgradeabilityProxy
有一個(gè)upgradeToAndCall
方法專門來調(diào)用一些邏輯合約中的方法寝优,一旦代理合約升級到最新版本時(shí),就把連接到的那個(gè)邏輯合約里的初始設(shè)置重新設(shè)置一遍枫耳。
如何升級
- 部署一個(gè)新版本的邏輯合約(V2)乏矾,確保它繼承了上一個(gè)版本里的狀態(tài)變量結(jié)構(gòu)。
- 調(diào)用
ownedUpgradeabilityProxy
實(shí)例來升級到新版本合約的地址。
tips
這個(gè)方法很棒钻心,因?yàn)樗恍枰壿嫼霞s知道它是整個(gè)代理系統(tǒng)的一部分凄硼。
關(guān)于升級
重要:如果你的邏輯合約依賴自己的構(gòu)造器來設(shè)置一些初始狀態(tài)的話,這個(gè)過程在新版本的邏輯合約注冊到代理中時(shí)需要重新做一遍捷沸。舉個(gè)例子摊沉,邏輯合約繼承Zeppelin中的Ownable
合約,這很常見痒给。當(dāng)你的邏輯合約繼承Ownable
说墨,它也就繼承了Ownable
的構(gòu)造器,構(gòu)造器會(huì)在合約創(chuàng)建的時(shí)候就設(shè)置合約的所有者是誰侈玄。當(dāng)你讓代理合約來使用你的邏輯合約的時(shí)候婉刀,代理合約是不知道邏輯合約的所有者是誰的吟温。
升級代理合約的一種常見的模式就代理立即對邏輯合約調(diào)用一個(gè)初始化方法序仙。這個(gè)初始化方法應(yīng)該去模仿在構(gòu)造器中做的一些事情。同時(shí)你也想要一個(gè)標(biāo)識鲁豪,用來確保你不可以再次對同一個(gè)邏輯合約調(diào)用初始化方法潘悼。(只能調(diào)用一次)
你的邏輯合約看上去可能像下面這樣:
contract Token is Ownable {
...
bool internal _initialized;
function initialize(address owner) public {
require(!_initialized);
setOwner(owner);
_initialized = true;
}
...
}
當(dāng)然這取決于你的部署策略,你可以寫一個(gè)幫助部署的合約爬橡,或者你可以可以單獨(dú)部署代理合約和邏輯合約治唤。如果你單獨(dú)部署的話,你需要使用upgradeToAndCall
把代理合約鏈接到邏輯合約上糙申,這看上去就會(huì)像下面這樣:
const initializeData = encodeCall('initialize', ['address'], [tokenOwner])
await proxy.upgradeToAndCall(logicContract.address, initializeData, { from: proxyOwner })
結(jié)論
代理模式的概念已經(jīng)出來有一段時(shí)間了宾添,但是由于太復(fù)雜了、害怕引入安全漏洞以及繞過了區(qū)塊鏈不可變的特性柜裸,它還沒有被廣泛接受缕陕。過去的解決方法在關(guān)于未來版本的邏輯合約可以添加和修改的東西上有嚴(yán)格的限制,這很不靈活疙挺。但是很顯然扛邑,開發(fā)者對于可升級合約的需求很迫切。zeppelin提供并且測試了三種模式铐然,他們致力于幫助開發(fā)者架構(gòu)自己的項(xiàng)目蔬崩,引入可升級特性。
盡管代理模式的概念出來也有段時(shí)間了搀暑,但是它的應(yīng)用依然處在非常早期沥阳。很開心看到越來越多的高級DApp架構(gòu)通過這種方式得以實(shí)現(xiàn)。