NO IMAGE

原文地址: https://ethfans.org/posts/flexible-upgradability-for-smart-contracts

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

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

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

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

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) }
}
}
}

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

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

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

代理委託狀態

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

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

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

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

雖然儲存記憶體一致(兩個合約都定義了一個 uint256 變數),但變數名(num 與 n)並不一致。即使這些變數名不相同,但它們仍可以通過匹配儲存記憶體編譯成位元組碼。因此,當 Thing代理呼叫 ThingDelegate 的 incrementNum() 方法時,也會在 Thing 的狀態中增加 num 變數。

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

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

ShrimpCoin 與 TokenDelegate

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

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;
}

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

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

鍵值儲存

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

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

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;
}
}

該合約定義了三個對映的 mapping 結構。用於儲存 uint256 、 bool 以及 address 型別的資料。這些對映用最高階的鍵值是 msg.sender ,(msg.sender)是使用 set/get 函式執行寫或讀操作的智慧合約的地址。

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

_uintStorage
<shrimp_coin_address>
"totalSupply": 1000
<clam_token_address>
"totalSupply": 2000
_boolStorage
<shrimp_coin_address>
"isPaused": true
<clam_token_address>
"isPaused": false

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

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

獲取並設定鍵值對

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

可以呼叫可約使用如下程式碼設定 totalSupply 的值為 1000 :

_storage.setUint("totalSupply", 1000);

我們還可以設定更復雜的資料,例如對映。我們可以使用 keccak256() 方法建立一個雜湊鍵值,以便在 balances 對映中為 balanceHolder設定餘額:

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

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

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);
}
}

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

升級到 Mintable 代幣

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

ShrimpCoin 的所有者地址可以呼叫 upgradeTo() 函式使得代理指標指向 MintableTokenDelegate 例項(我們稱之為V2)。

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

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;
}
}

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

未來規劃

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

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

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

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

缺點:

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

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

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

感謝您的閱讀 🙂

參考文獻: