不升级,出了 bug 怎么办?可升级,智能合约还有什么信任度可言?估计很多区块链从业技术人员都会像我一样有这样的纠结与困惑。
这两天看到一篇文章 Upgradeability Is a Bug 感觉讲的不错。本文大部分内容也来自那篇文章。
0x01 智能合约因其去信任性而有用
如果我给你说我这边有个不错的投资机会,你今天给我 1 块钱,我明天会返还给你两块,你会不会投?好吧,也许你会看在我的面子上投上 1 块钱,不是因为你相信我,而是因为 1 块钱即使丢了也无所谓。恐怕很少人愿意为了明天的 200万 而在今天付 100 万给我,跑路风险太大了,即使我们签个书面合约。即使你相信我的人品不会跑路,也会有很多很多的质疑,这就是信任的成本。
这个时候,我在以太坊上部署一个下面的智能合约,并往里面放 1000 个以太币,你知道智能合约一旦部署就是不可更改的,里面的代码逻辑清清楚楚写着你现在投资合约所存以太币的一半,也就是 500 个以太币,24 小时后就可以把合约里全部的 1000 个以太币全部取出。你还犹豫什么?赶紧投啊。
// 投资机会合约
contract InvestmentOpportunity {
address public investor;
uint256 public payday;
// 有了这个构造函数,我可以在部署合约时放若干个以太币进去
constructor() public payable {}
// 你可以调用这个函数进行投资
function invest() external payable {
// 如果有人投过了就不能再投了
require(investor == address(0), "Someone beat you to it!");
// 投资数额应是我所放到合约里的以太币数量的一半
require(msg.value == address(this).balance / 2,
"You must match the contract balance.");
// 记录投资人
investor = msg.sender;
// 当前时间加 24 小时作为投资兑现时间
payday = now + 24 hours;
}
// 你可以通过这个函数取钱
function withdraw() external {
// 确保取钱人是投资人
require(msg.sender == investor,
"Only the investor can withdraw.");
// 过了投资兑现时间才能把钱取出
require(now >= payday,
"You must wait until the payday time.");
// 将合约里的钱赚到你的账户里
msg.sender.transfer(address(this).balance);
}
}
这个时候,智能合约已经帮你我完成了所谓的去信任。你不需要相信我,只需要确保合约是好的就成了。这合约和纸质的合约不同,你不需要担心执行问题。
0x02 不可变性是去信任的基础
我们再来回顾一下上面的案例,为啥我口述一个回报那么丰厚的投资机会,你还会担心犹豫不敢投?担心我会变卦呗,你会从我的历史,我的动机各个方面分析后,确保我有明确的自私性理由才有可能肯相信我是真的不是信口开河,不是会拿钱跑路而是信守承诺。有时候单纯的做好事是很难的......
所谓信任度,很多时候就是取决于对变化的一种担心程度。
而基于区块链的智能合约给人们提供了一种选择,可以将合约内容和合约的执行都以不可变的形式确定下来。
0x03 合约的可升级会破坏不可变性
然而是程序就机会不可避免会有 bug,并且这种 bug 出现的几率会随着程序复杂度的增加而增加。常见的修复 bug 的方式就是对程序进行升级,所以很多人乐此不疲的研究可升级的合约设计,并总结出一些合约升级模式。
比如上面我们提到的那个合约,改成可升级的可能会变成下面这个样子。
基础合约保存了核心数据。
contract Base {
// proxy state
address owner;
address implementation;
// implementation state
address public investor;
uint256 public payday;
}
实现合约提供了投资逻辑的实现。
contract Implementation is Base {
function invest() external payable {
require(investor == address(0), "Someone beat you to it!");
require(msg.value == address(this).balance / 2,
"You must match the contract balance.");
investor = msg.sender;
payday = now + 24 hours;
}
function withdraw() external {
require(msg.sender == investor,
"Only the investor can withdraw.");
require(now >= payday,
"You must wait until the payday time.");
msg.sender.transfer(address(this).balance);
}
}
代理合约继承了基础合约,并可以通过 setImplementation
函数来更换业务逻辑实现合约。
contract Proxy is Base {
constructor(address _implementation) public payable {
owner = msg.sender;
implementation = _implementation;
}
function setImplementation(address _implementation) external {
require(msg.sender == owner);
implementation = _implementation;
}
function() external payable {
address impl = implementation;
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) }
}
}
}
投资逻辑没变,但这个时候让你通过代理合约来投资你会投吗?作为一个聪明又理性的人,你很可能会拒绝投资。因为现在智能合约带来的那种不可变性已经不复存在了,你不知道这个合约会不会按照当前的逻辑执行,因为我可以随时把业务逻辑实现合约替换掉。
如果我把业务逻辑实现合约替换为下面这个,随时可以卷钱跑路,智能合约又能起到什么保护作用!
contract ExitScam is Base {
function exit() external {
require(msg.sender == owner);
selfdestruct(msg.sender);
}
}
也许作为合约开发者,我们基本不会这么做,但是,谁会信呢?我们使用智能合约,不就是为了让用户不必去信任我们么?!
0x04 应该怎么办?
难道有 bug 就不改了么?难道合约被黑客攻击了就眼睁睁看着资产流失么?
非也。
- 我们可以在合约上添加业务暂停的开关,有异常发生时暂停业务。
- 与其设计可升级的合约,不如设计可迁移的合约。当问题发生时,部署新的合约,并将老合约的状态数据迁移至新合约。
- 如果实在需要可升级合约,尽量限制升级合约所影响的范围。进一步也可以使用多签机制,当社区里超过一定数量的用户签名同意升级后才能触发升级操作。
网友评论