以太坊智能合約具有很強的不變性,使得我們能夠構建完全防篡改的應用程序,任何個人、公司或政府都不能篡改數據(信息)。每個參與者都遵循相同的規則,並且這些規則永遠都不會改變。

但是,說到底,這些規則都是由人創造的。而人類總是偶然會犯一點錯誤的。我們不可能從第一天就看到未來發展的完整畫面,並構造一個完全不需要適配或改進的完美系統。

爲了平衡不變性與靈活性,我們需要一種升級部署後的去中心化應用程序的機制。在本文中,我們將介紹如何使用一些簡單但有效的模式來實現這一點。

雖然我們將描述升級機制,但我們不會討論升級是如何觸發的。我們假設升級操作將由“所有者”執行。該“所有者”可以是一個單獨持有的地址、一個多簽名合約,或者一個複雜的去中心化自治組織(DAO)。

現有模式

Zeppelin Solutions 和 Aragon 團隊已經提出了一些非常有效的升級模式。我們借鑑了 Solidity 代理庫(Proxy Libraries in Solidity) (中譯本見文末超鏈接)以及 使用永久存儲升級智能合約(Smart Contract Upgradability Using Eternal Storage) 的代碼。

Dapp 可升級工具箱

**
**

在 Level K,我們把這些模式應用到我們的 Dapp 可升級工具箱(正在開發當中)中。該工具箱包含一些用於升級任何去中心化應用程序的核心合約。

樣例代碼

如果你不想繼續看本文了,只想看看代碼,那就去吧!這篇文章的所有代碼都在這裏:github.com/levelkdev/upgradability-blog-post

如何寫出可升級代幣

**
**

我們假設你已經對 ERC20 代幣以及使他們工作的代碼有一定的瞭解。如果之前沒有了解的話,你可以看一看 Zeppelin 的 ERC20 合約代碼,從而更好地理解(相關內容)。

假設我們要部署一個名爲 ShrimpCoin 的新代幣。至於用途麼,只能讓人們自己猜想一下了。

下面的結構圖展示了,ShrimpCoin 從標準代幣升級爲“mintable”(鑄幣廠)代幣的樣子:

所有這些都有詳細解釋,請往下看!

代理與委託合約

**
**

你會注意到 ShrimpCoin 是一個代理合約。這意味着當一個交易被髮送(例如 transfer()), ShrimpCoin 並不知道交易內指定函數,它會將交易代理到我們稱爲“委託”的合約中。

這可以通過原生 EVM 代碼實現,委託調用 ( delegatecall )。從 Solidity 文檔中可以看到,一個使用 delegatecall 的合約……

……可以在運行時動態地從不同地址加載代碼。存儲、當前地址以及餘額仍然是指發起調用的合約,只是代碼來自被調地址。

簡單地說,這意味着, ShrimpCoin 包含了我們委託合約(TokenDelegate)的全部功能。要升級 ShrimpCoin 的功能,我們只需要通知代理使用新的委託合約(我們例子中是 MintableTokenDelegate )。代理合約的代碼可能有些晦澀難懂(這有一些 EVM 彙編代碼):

[code]

pragma solidity ^0.4.18;import \"zeppelin-solidity/contracts/ownership/Ownable.sol\";contract Proxy is Ownable {  event Upgraded(address indexed implementation);  address internal _implementation;  function implementation() public view returns (address) {    return _implementation;  }  function upgradeTo(address impl) public onlyOwner {    require(_implementation != impl);    _implementation = impl;    Upgraded(impl);  }  function () payable public {    address _impl = implementation();    require(_impl != address(0));    bytes memory data = msg.data;    assembly {      let result := delegatecall(gas, _impl, add(data, 0x20), mload(data), 0, 0)      let size := returndatasize      let ptr := mload(0x40)      returndatacopy(ptr, 0, size)      switch result      case 0 { revert(ptr, size) }      default { return(ptr, size) }    }  }}

[/code]

-來自 https://blog.zeppelinos.org/smart-contract-upgradeability-using-eternal-storage/

我們來看 fallback (返回)函數 function() payable public{...,其可以用於處理所有未知功能簽名的交易。在函數內部,彙編代碼用於進行 delegatecall 調用。對於沒有返回值的函數可以使用簡單的舊版本 Solidity 實現。然而,delegatecall 調用僅返回單一值,用於表示調用成功或失敗。該彙編代碼塊獲得了代理交易的實際返回值,並返回給上層函數。

代理合約是一個 Ownable 合約,並允許預設一些可以執行 upgradeTo() 函數的所有者,這些所有者可以使用任何委託合約升級該合約。

代理委託狀態

**
**

當代理合約使用委託合約的功能時,代理合約將發生狀態改變。這意味着兩個合約需要定義相同的存儲內存。兩個合約在內存中定義的存儲順序需要、一致。

下面有一個例子用於說明本概念。假設把 Thing 設置爲使用 ThingDelegate 的功能:

[code]

contract Thing is Proxy {  uint256 num;  string name = \"Thing\";}contract ThingDelegate {  uint256 n;  function incrementNum() public {    n = n + 1;  }}

[/code]

這裏發生了一些有趣的事情……

雖然存儲內存一致(兩個合約都定義了一個 uint256 變量),但變量名(numn)並不一致。即使這些變量名不相同,但它們仍可以通過匹配存儲內存編譯成字節碼。因此,當 Thing 代理調用 ThingDelegateincrementNum() 方法時,也會在 Thing 的狀態中增加 num 變量。

此外,額外存儲和狀態的定義在這( string name = \"Thing\" ,字符串類型變量 name,內容爲 \”Thing\”)。該存儲空間不能被 ThingDelegate 修改。存儲的順序在這裏非常重要。如果變量 name 定義在變量 num 之前,那麼 incrementNum() 將會試圖給一個字符串加一。

我們很喜歡這個模式的地方是, ThingDelegate 不需要知道 Thing。一旦 ThingDelegate 部署完成,任何合約都可以將其作爲委託使用,因此 ThingDelegate 是可以公開使用的。實際上,任意已部署的合約都可以作爲委託使用,並且不需要這樣定義。

ShrimpCoin 與 TokenDelegate

**
**

讓我們來看一看稍微複雜一點的 ShrimpCoinTokenDelegate 功能,以及一些存儲輔助(類), StorageConsumer(存儲消費者)和 StorageStateful(存儲狀態):

[code]

contract ShrimpCoin is StorageConsumer, Proxy, DetailedToken {  function ShrimpCoin(KeyValueStorage storage_)    public    StorageConsumer(storage_)  {    name = \"ShrimpCoin\";    symbol = \"SHRMP\";    decimals = 18;  }}contract DetailedToken {  string public name;  string public symbol;  uint8 public decimals;}contract TokenDelegate is StorageStateful {  function totalSupply() public view returns (uint256) {    return _storage.getUint(\"totalSupply\");  }}contract StorageConsumer is StorageStateful {  function StorageConsumer(KeyValueStorage storage_) public {    _storage = storage_;  }}contract StorageStateful {  KeyValueStorage _storage;}

[/code]

遵循與 Thing 示例相同的模式。但這裏的通用狀態時 KeyValueStorage(鍵值存儲)合約(在下一部分講述)的地址。

需要特別強調的是,ShrimpCoin 在繼承 DetailedToken 之前繼承了 StorageConsumer。如果(繼承順序)交換, TokenDelegate 將會在 getUint() 操作中使用字符串命名(string name);而不是鍵值存儲(KeyValueStorage _storage)。這將導致交易回滾。

**

**

鍵值存儲

**
**

代理委託模式對於升級功能非常有用,但是如果我們想添加一些在原始合約中沒有定義的狀態呢?這就是“永恆存儲”模式的由來。這種模式最初在使用 Solidity 編寫可升級合約中提出。

下面是一個簡化的 KeyValueStorage (鍵值存儲)合約:

[code]

contract KeyValueStorage {  mapping(address => mapping(bytes32 => uint256)) _uintStorage;  mapping(address => mapping(bytes32 => address)) _addressStorage;  mapping(address => mapping(bytes32 => bool)) _boolStorage;  /****Get Methods***********/  function getAddress(bytes32 key) public view returns (address) {      return _addressStorage[msg.sender][key];  }  function getUint(bytes32 key) public view returns (uint) {      return _uintStorage[msg.sender][key];  }  function getBool(bytes32 key) public view returns (bool) {      return _boolStorage[msg.sender][key];  }  /****Set Methods***********/  function setAddress(bytes32 key, address value) public {    _addressStorage[msg.sender][key] = value;  }  function setUint(bytes32 key, uint value) public {      _uintStorage[msg.sender][key] = value;  }  function setBool(bytes32 key, bool value) public {      _boolStorage[msg.sender][key] = value;  }}

[/code]

該合約定義了三個映射的 mapping 結構。用於存儲 uint256bool 以及 address 類型的數據。這些映射用最高級的鍵值是 msg.sender ,(msg.sender)是使用 set/get 函數執行寫或讀操作的智能合約的地址。

邏輯上,鍵 / 值存儲結構如下:

[code]

_uintStorage         \"totalSupply\": 1000         \"totalSupply\": 2000_boolStorage         \"isPaused\": true         \"isPaused\": false

[/code]

在我們的例子中, msg.senderShrimpCoin 合約地址,而鍵值可能形如 \"totalSupply\"

由於我們正關閉 msg.sender,全部的鍵值對的範圍均被限定在發送者合約內。一個合約不能操縱其他合約的存儲數據。這意味着在 KeyValueStorage 合約部署之後,它對任何合約開放使用。

獲取並設置鍵值對

**
**

我們可以使用 KeyValueStorage 提供的 getter 和 setter 方法讀取或設置狀態值。

可以調用可約使用如下代碼設置 totalSupply 的值爲 1000

[code]

_storage.setUint(\"totalSupply\", 1000);

[/code]

我們還可以設置更復雜的數據,例如映射。我們可以使用 keccak256() 方法創建一個哈希鍵值,以便在 balances 映射中爲 balanceHolder 設置餘額:

[code]

_storage.setUint(keccak256(\"balances\", balanceHolder), amount);

[/code]

這些低級存儲函數比經常使用的 \"balances[address] = amount\" ;語法更冗長複雜,因此將它們封裝在一些更高級的函數中更有意義。下面來看看 TokenDelegate 中是如何實現的:

[code]

contract TokenDelegate is StorageStateful {  using SafeMath for uint256;  function transfer(address to, uint256 value) public returns (bool) {    require(to != address(0));    require(value <= getBalance(msg.sender));    subBalance(msg.sender, value);    addBalance(to, value);    return true;  }  function balanceOf(address owner) public view returns (uint256 balance) {    return getBalance(owner);  }  function getBalance(address balanceHolder) public view returns (uint256) {    return _storage.getUint(keccak256(\"balances\", balanceHolder));  }  function totalSupply() public view returns (uint256) {    return _storage.getUint(\"totalSupply\");  }  function addSupply(uint256 amount) internal {    _storage.setUint(\"totalSupply\", totalSupply().add(amount));  }  function addBalance(address balanceHolder, uint256 amount) internal {    setBalance(balanceHolder, getBalance(balanceHolder).add(amount));  }  function subBalance(address balanceHolder, uint256 amount) internal {    setBalance(balanceHolder, getBalance(balanceHolder).sub(amount));  }  function setBalance(address balanceHolder, uint256 amount) internal {    _storage.setUint(keccak256(\"balances\", balanceHolder), amount);  }}

[/code]

類似於 getBalance() 的內部函數,能使餘額存儲變得更容易。該功能可以進一步重構到代碼庫中,以便在多個委託合約間共享。

升級到 Mintable 代幣

**
**

假設我們使用一個指向 TokenDelegate 的代理指針部署 ShrimpCoin (我們稱之爲 V1)。由於 TokenDelegate 不能提供初始化創建機制或“鑄幣”的代幣,V1 的實現是受限的。

ShrimpCoin 的所有者地址可以調用 upgradeTo() 函數使得代理指針指向 MintableTokenDelegate 實例(我們稱之爲 V2)。

V2 MintableTokenDelegate 合約提供了一些鑄幣的額外功能,可以操作一組全新的存儲鍵值:

[code]

contract MintableTokenDelegate is TokenDelegate {  modifier onlyOwner {    require(msg.sender == _storage.getAddress(\"owner\"));    _;  }  modifier canMint() {    require(!_storage.getBool(\"mintingFinished\"));    _;  }  function mint(address to, uint256 amount) onlyOwner canMint public returns (bool) {    addSupply(amount);    addBalance(to, amount);    return true;  }  function finishMinting() onlyOwner canMint public returns (bool) {    _storage.setBool(\"mintingFinished\", true);    return true;  }}

[/code]

它還繼承了 V1 TokenDelegate 的所有功能,因此像 ShrimpCoin 這樣正代理的合約不會失去任何原始功能。

未來規劃

**
**

我們已經推出一個代幣升級的簡單示例,但該方式也可以應用到更復雜的情景中。我們在 Level K 上發佈的一個令人興奮的用例是可升級 代幣策劃註冊表。

這些模式提供了一些非常酷的機會,可以爲通用功能開發和部署可重用的委託合約,可以通過一個潛在的大規模多樣化去中心化應用程序組來加以利用。

使用組合代理委託及鍵值存儲的升級模式的優點有:

  • 對於功能和存儲升級提供全靈活性
  • 鼓勵封裝通用功能的標準合約的創建與部署
  • 使用預先部署的智能合約當做委託不容易出錯(將在將來進行全面測試)。
  • 重複利用預先部署的智能合約意味着更簡單的審計,並減少部署所需 gas 成本。

缺點:

  • 升級“所有者”擁有完全控制權,意味着完全信任。爲設計一個真正去信任的(Trustless)、也可升級的合約,“所有者”自身必須是一個去信任的合約。
  • 簡直存儲操作的語法與標準 Solidity 狀態變量操作更復雜。
  • 標準共享合約中的一個缺陷可能波及到所有使用該合約的去中心化應用程序。

我們很期待聽到其他開發者使用這些模式。 Rocket Pool 項目正在用可升級性做一些非常 Amazing 的事情。我們很期待聽到其他人的聲音!

如果您發現此篇文章有幫助,請告訴我們!到 [email protected] 來表達你們的愛吧!

感謝您的閱讀 🙂

_ 參考文獻:_


原文鏈接 :https://medium.com/level-k/flexible-upgradability-for-smart-contracts-9778d80d1638
作者 :Mike Calvanese
翻譯 & 校對 :賈林鵬 & Elisa

_
_

_ 本文由作者授權 EthFans 翻譯及再出版。_

_
_


你可能還會喜歡:

乾貨 | 以太坊可更新智能合約研究與開發綜述
乾貨 | Solidity 中的代理庫
乾貨 | 理解 ERC-20 token 合約

來源鏈接:None