代理合約用於將資料和呼叫轉發到邏輯合約。使用代理合約,使用者可以始終呼叫相同的合約地址,並且將其簡單地轉發到當前邏輯合約。
這種方法透過使用DELEGATECALL操作碼來工作。DELEGATECALL是EVM提供的用於程式集的操作碼。它的工作方式與普通呼叫類似,只是目標地址的程式碼是在呼叫協定的上下文中執行的。這意味著像“msg.sender”和“msg.value”這樣的值將被保留。實際上,DELEGATECALL允許目標協定代表被呼叫方進行呼叫。
contract Relay {
address public currentVersion;
address public owner;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function Relay(address initAddr) {
currentVersion = initAddr;
owner = msg.sender; // this owner may be another contract with multisig, not a single contract owner
}
function changeContract(address newVersion) public
onlyOwner()
{
currentVersion = newVersion;
}
function() {
require(currentVersion.delegatecall(msg.data));
}
}
儘管這種方法避免了與登錄檔合同有關的問題,但它也有其自身的問題。 例如如果管理不當,資料儲存很容易失敗。如果新合約的儲存佈局與以前的合約不同,則資料可能已損壞。此實現還防止您從函式接收返回值,從而限制了其用例。
儲存合約
與以前的方法一樣,此方法需要您的邏輯合約以及輔助合約。在這種情況下,輔助合約是永久儲存合約。該技術透過分離邏輯和資料來起作用。邏輯合約可以隨時升級,並且由於資料儲存在外部,因此您的資料受到保護。
當然,這種方法也存在根本缺陷。如果在儲存合約中發現錯誤或漏洞,則在不破壞當前資料儲存的情況下無法對其進行升級。 這種方法的另一個問題是邏輯協定需要使用額外的氣體來進行外部呼叫以檢視或修改資料。
更合適的升級方法
現在讓我們來看看一些更復雜、更合適的智慧合約升級方法。
繼承的儲存可升級性
這種技術使用三種不同的合約:代理合約來委託呼叫並充當永久儲存;邏輯合約將處理資料;還有儲存合約。代理合約和邏輯合約都繼承自儲存合約,因此它們的儲存引用是對齊的。
當邏輯合約更新時,我們只需要更改代理合約所指向的位置即可使用僅管理員功能。由於代理和邏輯協定具有相同的儲存指標,因此無需進行外部呼叫即可檢視和修改資料。
不幸的是,這種方法也有其自身的陷阱。由於代理合約和儲存合約都是永恆的,因此,如果在任何一個合約中發現錯誤或漏洞,都無法修復。 因此務必仔細考慮您的代理和儲存結構。
非結構化儲存可升級性
非結構化儲存可能是當前最大的可升級性方法,它使我們能夠利用儲存中狀態變數的佈局。此方法僅需要兩個合約-代理合約和實施合約-實施合約包含資料和儲存。
該技術的工作原理是將可升級性所需的資料儲存在儲存中的固定位置,以防止被新資料覆蓋。我們可以使用SLOAD和SSTORE操作碼進行彙編。由於儲存插槽只是從0x0開始遞增,因此我們使用很高的儲存插槽來防止覆蓋 我們可以透過對常量變數進行雜湊來生成儲存槽。 由於恆定狀態變數不會佔用儲存空間,因此我們不必擔心它會被覆蓋。
bytes32 private constant implementationPosition =
keccak256("org.zeppelinos.proxy.implementation");
由於代理不再從儲存合約繼承而來,因此我們現在也可以更新儲存,從而防止儲存錯誤/漏洞變成災難性的。 但是在升級實施合約時,我們必須繼承以前的合約。由於不需要更改實施合約,因此該方法甚至可以與現有合約一起使用。
儘管這可能是當前可升級性最好的方法,但也有不少批評。代理所有者擁有巨大的權力,並且需要一定程度的信任。對於更復雜的系統,這可能也不是合適的解決方案。
升級依賴於建構函式的合約
當使用依賴於建構函式的合約來設定一些初始狀態時,與代理工作並不太簡單。由於建構函式只執行一次,而代理不知道邏輯合約建構函式中設定的值,因此我們需要一種方法在代理中初始化其中的一些值。
建立邏輯合約後,EVM會丟棄建構函式,因此我們不能簡單地重用程式碼。相反,我們必須採取獨特的方法來解決此問題。
初始化函式
一種可能的替代方法是在常規函式中使用建構函式程式碼。我們只需確保這個函式(我們將呼叫初始化函式)只能執行一次。
contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
*/
bool private initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private initializing;
/**
* @dev Modifier to use in the initializer function of a contract.
*/
modifier initializer() {
require(initializing || !initialized, "Contract instance has already been initialized");
bool wasInitializing = initializing;
initializing = true;
initialized = true;
_;
initializing = wasInitializing;
}
}
在使用初始值設定項函式時,必須打起十二分精神。考慮邏輯合約繼承的基本合約也很重要。這部分特別複雜,因為Solidity也支援多重繼承。
結論
確保智慧合約是可升級的,並仔細考慮可升級過程,這兩點都很重要。雖然這並不是一個關於智慧合約可升級性的選項的詳盡列表,但這應該是關於這個主題的適當指南。