本文主要說明以太坊的登錄檔合約、代理合約、繼承的儲存可升級性,以及更多的可升級性方法。
在軟體工程中,當發現新的bug和安全風險時,通常會對它們進行修補,並實時推送更新的版本。在智慧合約開發中,可升級性並不是那麼簡單。因此,我們必須採取不同的做法。
以太坊仍處於起步階段,關於如何升級智慧合約版本的爭議很多,但我們將介紹一些當今最好的選擇。
注意:智慧合約版本的可升級性仍然是研究的活躍領域。以下任何一種方法都可能由於濫用或新發現的漏洞而導致智慧合約失敗。
智慧合約可升級性的基本方法
在這裡,我們將介紹一些更平易近人但不太適合的智慧合約可升級性解決方案。儘管這些不是最佳方法,但它們是當今使用的核心。
註冊合約
登錄檔合約可能是最簡單的可升級性方法,但是在這種方法,簡單性帶來了一些嚴重的缺陷。
它使用兩個智慧合約的工作:登錄檔合約和邏輯合約。登錄檔協定僅用於將使用者指向邏輯協定的當前版本。每當邏輯合約被升級時,登錄檔合約的所有者就可以更新邏輯合約被升級的地址。
contractSomeRegister{addressbackendContract;address[]previousBackends;addressowner;functionSomeRegister(){owner=msg.sender;}modifieronlyOwner(){require(msg.sender==owner)_;}functionchangeBackend(addressnewBackend)publiconlyOwner()returns(bool){if(newBackend!=backendContract){previousBackends.push(backendContract);backendContract=newBackend;returntrue;}returnfalse;}}
這種方法是非常不利的,因為當使用者想要使用合約時,他們必須首先查詢當前地址。否則可能導致資金損失。將資料遷移到新合約中也非常困難,因此必須仔細考慮此過程以避免失敗。
代理合約
代理合約用於將資料和呼叫轉發到邏輯合約。使用代理合約,使用者可以始終呼叫相同的合約地址,並且將其簡單地轉發到當前邏輯合約。
這種方法透過使用DELEGATECALL操作碼來工作。DELEGATECALL是EVM提供的用於程式集的操作碼。它的工作方式與普通呼叫類似,只是目標地址的程式碼是在呼叫協定的上下文中執行的。這意味著像“msg.sender”和“msg.value”這樣的值將被保留。實際上,DELEGATECALL允許目標協定代表被呼叫方進行呼叫。
contractRelay{addresspubliccurrentVersion;addresspublicowner;modifieronlyOwner(){require(msg.sender==owner);_;}functionRelay(addressinitAddr){currentVersion=initAddr;owner=msg.sender;//thisownermaybeanothercontractwithmultisig,notasinglecontractowner}functionchangeContract(addressnewVersion)publiconlyOwner(){currentVersion=newVersion;}function(){require(currentVersion.delegatecall(msg.data));}}
儘管這種方法避免了與登錄檔合同有關的問題,但它也有其自身的問題。 例如如果管理不當,資料儲存很容易失敗。如果新合約的儲存佈局與以前的合約不同,則資料可能已損壞。此實現還防止您從函式接收返回值,從而限制了其用例。
儲存合約
與以前的方法一樣,此方法需要您的邏輯合約以及輔助合約。在這種情況下,輔助合約是永久儲存合約。該技術透過分離邏輯和資料來起作用。邏輯合約可以隨時升級,並且由於資料儲存在外部,因此您的資料受到保護。
當然,這種方法也存在根本缺陷。如果在儲存合約中發現錯誤或漏洞,則在不破壞當前資料儲存的情況下無法對其進行升級。 這種方法的另一個問題是邏輯協定需要使用額外的氣體來進行外部呼叫以檢視或修改資料。
更合適的升級方法
現在讓我們來看看一些更復雜、更合適的智慧合約升級方法。
繼承的儲存可升級性
這種技術使用三種不同的合約:代理合約來委託呼叫並充當永久儲存;邏輯合約將處理資料;還有儲存合約。代理合約和邏輯合約都繼承自儲存合約,因此它們的儲存引用是對齊的。
當邏輯合約更新時,我們只需要更改代理合約所指向的位置即可使用僅管理員功能。由於代理和邏輯協定具有相同的儲存指標,因此無需進行外部呼叫即可檢視和修改資料。
不幸的是,這種方法也有其自身的陷阱。由於代理合約和儲存合約都是永恆的,因此,如果在任何一個合約中發現錯誤或漏洞,都無法修復。 因此務必仔細考慮您的代理和儲存結構。
非結構化儲存可升級性
非結構化儲存可能是當前最大的可升級性方法,它使我們能夠利用儲存中狀態變數的佈局。此方法僅需要兩個合約-代理合約和實施合約-實施合約包含資料和儲存。
該技術的工作原理是將可升級性所需的資料儲存在儲存中的固定位置,以防止被新資料覆蓋。我們可以使用SLOAD和SSTORE操作碼進行彙編。由於儲存插槽只是從0x0開始遞增,因此我們使用很高的儲存插槽來防止覆蓋 我們可以透過對常量變數進行雜湊來生成儲存槽。 由於恆定狀態變數不會佔用儲存空間,因此我們不必擔心它會被覆蓋。
bytes32privateconstantimplementationPosition=keccak256("org.zeppelinos.proxy.implementation");由於代理不再從儲存合約繼承而來,因此我們現在也可以更新儲存,從而防止儲存錯誤/漏洞變成災難性的。 但是在升級實施合約時,我們必須繼承以前的合約。由於不需要更改實施合約,因此該方法甚至可以與現有合約一起使用。
儘管這可能是當前可升級性最好的方法,但也有不少批評。代理所有者擁有巨大的權力,並且需要一定程度的信任。對於更復雜的系統,這可能也不是合適的解決方案。
升級依賴於建構函式的合約
當使用依賴於建構函式的合約來設定一些初始狀態時,與代理工作並不太簡單。由於建構函式只執行一次,而代理不知道邏輯合約建構函式中設定的值,因此我們需要一種方法在代理中初始化其中的一些值。
建立邏輯合約後,EVM會丟棄建構函式,因此我們不能簡單地重用程式碼。相反,我們必須採取獨特的方法來解決此問題。
初始化函式一種可能的替代方法是在常規函式中使用建構函式程式碼。我們只需確保這個函式(我們將呼叫初始化函式)只能執行一次。
contractInitializable{/***@devIndicatesthatthecontracthasbeeninitialized.*/boolprivateinitialized;/***@devIndicatesthatthecontractisintheprocessofbeinginitialized.*/boolprivateinitializing;/***@devModifiertouseintheinitializerfunctionofacontract.*/modifierinitializer(){require(initializing||!initialized,"Contractinstancehasalreadybeeninitialized");boolwasInitializing=initializing;initializing=true;initialized=true;_;initializing=wasInitializing;}}
在使用初始值設定項函式時,必須打起十二分精神。考慮邏輯合約繼承的基本合約也很重要。這部分特別複雜,因為Solidity也支援多重繼承。
結論
確保智慧合約是可升級的,並仔細考慮可升級過程,這兩點都很重要。雖然這並不是一個關於智慧合約可升級性的選項的詳盡列表,但這應該是關於這個主題的適當指南。