智能合约升级
原文地址: 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) }
}
}
}
我们来看 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的事情。我们很期待听到其他人的声音!如果您发现此篇文章有帮助,请告诉我们!到 contact@levelk.io 来表达你们的爱吧!感谢您的阅读 :-)参考文献:
- 全ShrimpCoin例子
- Level K 去中心化应用程序升级工具箱(正在开发中)
- Zeppelin Solutions and Aragon: Solidity 代理代码库(编者注:中译本见文末)
- 使用永久存储升级智能合约
- Rocket Pool:可升级 Solidity 合约设计
- Colony:使用Solidity编写可升级合约
阅读更多
- 如何编写一个可升级的智能合约
- 如何编写一个可升级的智能合约
- Solidity智能合约升级解决方案
- 如何编写皇冠体育足球竞猜网站开发一个可升级的智能合约
- 经典:浅谈以太坊智能合约的设计模式与升级方法
- 比特币和公共块链会加速智能合约的…
- Scry中文名首发:無域——开启基于区块链数据化经济的智能合约时代
- 智能合约开发solidity编程语言实例
- 五一劳动节,讲讲NEO智能合约的调试
- 以太坊智能合约Hello World示例程序
- 一个简单的智能合约模型
- Hyperledger Fabric 链码(智能合约)基本操作
- 智能合约案例集|资例子
- 以太坊智能合约入门项目-众筹项目
- 动态|黑客利用EOS智能合约漏洞从赌博应用EOSBet中窃取20万美元
- 以太坊构建节点集群流程,多节点智能合约,常见错误及处理手段
- SAP云平台,区块链,超级账本和智能合约
- 中国商业智能网的 blog 升级
- 第三章:版权交易系统智能合约开发-CSDN就业班-专题视频课程
- 智能合约开发环境搭建及Hello World合约