如何實現可升級的智慧合約?
智慧合約的重要性已越來越明顯,現如今,整個密碼貨幣生態系統都是由智慧合約所驅動!不管我們有多小心,或者我們的程式碼測試工作做得有多好,如果我們建立的是一個複雜的系統,那我們就有必要更新合約邏輯,以修補其存在的漏洞,或者新增必要的缺失功能。有時候,由於EVM虛擬機器的更改或者被新發現的漏洞,我們可能需要去升級我們的智慧合約。
一般來說,開發人員可以很容易地升級他們的軟體,但區塊鏈的情況是不一樣滴,因為它們有著難以更改的屬性。如果我們部署了一個合約,這就好比是潑出去的水。然而,如果我們使用適當的技術,我們可以在不同的地址部署一個新的合約,並使得舊合約無效。下面是一些最常見的,建立可升級智慧合約的方法。
主從合約(Master-Slave contract)
主從技術,是可實現升級智慧合約最為基礎也是最容易理解的技術之一。在這種技術當中,我們部署一個主合約,以及其他合約,其中主合約負責儲存所有其他合約的地址,並在需要時返回所需的地址。當這些合約需要和其它合約進行溝通時,它們會充當從合約,從主合約那裡獲取其它合約的最新地址。為了升級智慧合約,我們只需要在網路上部署它,並更改主合約中的地址。雖然這遠不是發展可升級智慧合約的最佳方式,但它確是最簡單的。這種方法存在著很多的侷限性,其中之一是,我們不能輕易地把合約的資料或資產遷移到新合約中。
永久儲存合約(Eternal Storage contract)
在這種技術當中,我們將邏輯合約和資料合約彼此分離。資料合約應該是永久並且不可升級的。而邏輯合約可以根據需要進行多次升級,並將變化通知給資料合約。這是一項相當基本的技術,並且存在著一個明顯的缺陷。由於資料合約是不可升級的,資料結構中需要的任何更改,或資料合約中存在的漏洞,都會導致所有資料變得無用。這種技術的另一個問題是,如果邏輯合約想要訪問/操作區塊鏈上的資料,那麼這個邏輯合約將需要進行外部呼叫,而外部呼叫會消耗額外的gas。通常情況下,這種技術會和主從技術相結合,以促進合約間的通訊。
可升級儲存代理合約
我們可通過使永久儲存合約充當邏輯合約的代理,以此防止支付額外的gas。這個代理合約,以及這個邏輯合約,將繼承同一儲存合約,那麼它們的儲存會在EVM虛擬機器中對齊。這個代理合約將有一個回退函式,它將委託呼叫這個邏輯合約,那麼這個邏輯合約就可以在代理儲存中進行更改。這個代理合約將是永恆的。這節省了對儲存合約多次呼叫所需的gas,不管資料做了多少的更改,就只需要一次委託呼叫。
這項技術當中有三個組成部分:
- 代理合約(Proxy contract) :它將充當永久儲存並負責委託呼叫邏輯合約;
- 邏輯合約(Logic contract) :它負責完成處理所有的資料;
- 儲存結構(Storage structure) :它包含了儲存結構,並會由代理合約和邏輯合約所繼承,以便它們的儲存指標能夠在區塊鏈上保持同步;
委託呼叫
該技術的核心在於EVM所提供的 DELEGATECALL
操作碼, DELEGATECALL
就像是一個普通的 CALL
呼叫操作碼,不同之處在於目標地址上的程式碼是在呼叫合約上下文中執行的,而原始呼叫的msg.sender以及msg.value將被保留。簡單說, DELEGATECALL
基本上允許(委託)目標合約在呼叫合約的儲存中做它任何想做的事情。
我們將利用這一點,並建立一個代理合約,它將使用 DELEGATECALL
操作碼委託呼叫邏輯合約,這樣我們就可以在代理合約中保持資料的安全,同時我們可以自由地更改邏輯合約。
如何使用可升級儲存代理合約?
讓我們深入研究一下細節。我們需要的第一個合約是儲存結構。它將定義我們需要的所有儲存變數,並將由代理合約和執行合約所繼承。它看起來會是這樣的:
contract StorageStructure { address public implementation; address public owner; mapping (address => uint) internal points; uint internal totalPlayers; }
我們現在需要一個執行/邏輯合約。讓我們建立一個簡單版的合約,在新增新玩家時不會增加totalPlayers計數器的數字。
contract ImplementationV1 is StorageStructure { modifier onlyOwner() { require (msg.sender == owner); _; } function addPlayer(address _player, uint _points) public onlyOwner { require (points[_player] == 0); points[_player] = _points; } function setPoints(address _player, uint _points) public onlyOwner { require (points[_player] != 0); points[_player] = _points; } }
下面就是最關鍵的部分:代理合約;
contract Proxy is StorageStructure { modifier onlyOwner() { require (msg.sender == owner); _; } /** * @dev constructor that sets the owner address */ constructor() public { owner = msg.sender; } /** * @dev Upgrades the implementation address * @param _newImplementation address of the new implementation */ function upgradeTo(address _newImplementation) external onlyOwner { require(implementation != _newImplementation); _setImplementation(_newImplementation); } /** * @dev Fallback function allowing to perform a delegatecall * to the given implementation. This function will return * whatever the implementation call returns */ function () payable public { address impl = implementation; 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) } } } /** * @dev Sets the address of the current implementation * @param _newImp address of the new implementation */ function _setImplementation(address _newImp) internal { implementation = _newImp; } }
為了讓合約生效,我們首先需要部署代理合約以及ImplementationV1合約,然後呼叫這個代理合約的 upgradeTo(address)函式
,同時pass掉我們的ImplementationV1合約地址。現在,我們可以忘記這個ImplementationV1合約的地址,並把代理合約的地址作為我們的主地址。
為了升級這個合約,我們需要建立一個新的邏輯合約實現,它可以是這樣的:
contract ImplementationV2 is ImplementationV1 { function addPlayer(address _player, uint _points) public onlyOwner { require (points[_player] == 0); points[_player] = _points; totalPlayers++; } }
你應該注意到,這個合約也繼承了儲存結構合約(StorageStructure contract),儘管它是間接地。
所有的執行方案都必須繼承這個儲存結構合約,並且在部署代理合約後不得進行更改,以避免對代理的儲存進行意外覆蓋。
為了實現升級,我們在網路上部署這個合約,然後呼叫代理合約的 upgradeTo(address)
函式,同時pass掉ImplementationV2合約的地址。
這種技術,使得升級合約邏輯變得相當容易,但它仍然不允許我們升級合約的儲存結構。我們可以通過使用非結構化的代理合約來解決這個問題。
非結構化可升級儲存代理合約
這是當前最先進的,可實現智慧合約升級的方法之一。它通過儲存合約地址以及在儲存中固定位置所有者的方法,以實現它們不會被執行/邏輯合約提供的資料所覆蓋。我們可以使用 sload
以及 sstore
操作碼來直接讀取和寫入由固定指標所引用的特定儲存槽。
此方法利用了儲存中狀態變數的佈局,以避免邏輯合約覆蓋掉固定位置。如果我們將固定位置設定為 0x7
,那麼在使用前7個儲存槽後,它就會被覆蓋掉。為了避免這種情況,我們將固定位置設定為類似 keccak256(“org.govblocks.implemenation.address”)
.
這消除了在代理合約中繼承儲存結構合約的需要,這意味著我們現在也可以升級儲存結構了。然而,升級儲存結構是一項棘手的任務,因為我們需要確保,我們所提交的更改,不會導致新的儲存佈局與先前的儲存佈局不匹配。
這項技術有兩個組成部分。
1、代理合約:它負責將執行合約的地址儲存在一個固定的地址當中,並負責委託呼叫它;
2、執行合約:它是主要合約,負責把我邏輯以及儲存結構;
你甚至可以將這項技術用於你現有的合約,因為它不需要對你的執行合約進行任何更改。
這個代理合約會是這樣子的:
contract UnstructuredProxy { // Storage position of the address of the current implementation bytes32 private constant implementationPosition = keccak256("org.govblocks.implementation.address"); // Storage position of the owner of the contract bytes32 private constant proxyOwnerPosition = keccak256("org.govblocks.proxy.owner"); /** * @dev Throws if called by any account other than the owner. */ modifier onlyProxyOwner() { require (msg.sender == proxyOwner()); _; } /** * @dev the constructor sets owner */ constructor() public { _setUpgradeabilityOwner(msg.sender); } /** * @dev Allows the current owner to transfer ownership * @param _newOwner The address to transfer ownership to */ function transferProxyOwnership(address _newOwner) public onlyProxyOwner { require(_newOwner != address(0)); _setUpgradeabilityOwner(_newOwner); } /** * @dev Allows the proxy owner to upgrade the implementation * @param _implementation address of the new implementation */ function upgradeTo(address _implementation) public onlyProxyOwner { _upgradeTo(_implementation); } /** * @dev Tells the address of the current implementation * @return address of the current implementation */ function implementation() public view returns (address impl) { bytes32 position = implementationPosition; assembly { impl := sload(position) } } /** * @dev Tells the address of the owner * @return the address of the owner */ function proxyOwner() public view returns (address owner) { bytes32 position = proxyOwnerPosition; assembly { owner := sload(position) } } /** * @dev Sets the address of the current implementation * @param _newImplementation address of the new implementation */ function _setImplementation(address _newImplementation) internal { bytes32 position = implementationPosition; assembly { sstore(position, _newImplementation) } } /** * @dev Upgrades the implementation address * @param _newImplementation address of the new implementation */ function _upgradeTo(address _newImplementation) internal { address currentImplementation = implementation(); require(currentImplementation != _newImplementation); _setImplementation(_newImplementation); } /** * @dev Sets the address of the owner */ function _setUpgradeabilityOwner(address _newProxyOwner) internal { bytes32 position = proxyOwnerPosition; assembly { sstore(position, _newProxyOwner) } } }
如何使用非結構化可升級儲存代理合約?
使用非結構化可升級儲存代理合約是非常簡單的,因為這種技術幾乎可以處理所有現有的合約。想要使用這種技術,你只需要遵循以下步驟:
upgradeTo(address)
我們現在可以忘掉這個執行合約地址,然後把代理合約的地址作為主地址。
而要升級這個新實施的合約,我們只需要部署新的執行合約,並呼叫代理合約的 upgradeTo(address)
函式,同時pass掉這個新執行合約的地址。就是這麼簡單!
讓我們簡單舉個例子。我們將再次使用上述可升級儲存代理合約中使用的同一邏輯合約,但是我們不需要用到儲存結構。因此,我們的ImplementationV1合約看起來會是這樣的:
contract ImplementationV1 { address public owner; mapping (address => uint) internal points; modifier onlyOwner() { require (msg.sender == owner); _; } function initOwner() external { require (owner == address(0)); owner = msg.sender; } function addPlayer(address _player, uint _points) public onlyOwner { require (points[_player] == 0); points[_player] = _points; } function setPoints(address _player, uint _points) public onlyOwner { require (points[_player] != 0); points[_player] = _points; } }
下一步是部署這個執行合約以及我們的代理合約。然後,再呼叫代理合約的 upgradeTo(address)
函式,同時pass掉執行合約的地址。
你可能注意到,在這個執行合約中,甚至沒有宣告totalPlayers變數,我們可以升級這個執行合約,其中具有 totalPlayers變數,這個新的執行合約看起來會是這樣的:
contract ImplementationV2 is ImplementationV1 { uint public totalPlayers; function addPlayer(address _player, uint _points) public onlyOwner { require (points[_player] == 0); points[_player] = _points; totalPlayers++; } }
而要升級這個新的執行合約,我們需要做的,就是在網路上部署這個合約,然後,嗯你猜對了,就是呼叫代理合約的 upgradeTo(address)
函式,並同時pass掉我們新執行合約的地址。現在,我們的合約已演變為能夠保持跟蹤 totalPlayers,同時仍然為使用者提供相同的地址。
這種方法是強大的,但也存在著一些侷限性。主要關注的一點是,代理合約擁有者(proxyOwner)有太多的權力。而且,這種方法對複雜的系統而言是不夠的。 對於構建具有可升級合約的 dApp而言,組合主從合約以及非結構化可升級儲存代理合約,會是更為靈活的一種方法 ,這也是作者所在的GovBlocks所使用的方法。
結論
非結構化儲存代理合約,是建立可升級智慧合約最先進的技術之一,但它仍然是不完美的。畢竟,我們並不希望dApp所有者對dApp具有不正當的控制權。如果開發者擁有了這種權力,那這個dapp還能稱之為去中心化應用嗎?
歡迎你給出自己的看法。
發文時比特幣價格 ¥42871.86