文章讨论了智能合约中的可升级性问题,认为可升级性破坏了智能合约的不可变性,因此是一个缺陷。文章通过代码示例详细解释了如何使用代理模式实现可升级性,并提出了几种缓解策略,如限制可变性和使用参数等。
我想给你提供一个一生只有一次的投资机会。如果你今天发送 1 以太币给我,我明天会发送 2 以太币给你。你愿意吗?
尽管有一些 证据表明人们会接受这样的提议,但我希望你会拒绝。你没有任何理由相信我是否会履行承诺。
智能合约登场。智能合约让我们能够在不信任对方的情况下进行各种交易。下面是一个简单的以太坊智能合约,我们可以用它来提供这个投资机会。如果合约中有 1 以太币,你可以发送 1 以太币,并在明天提取 2 以太币:
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;
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);
}
}
如果我将该合约部署到以太坊主网并存入 1 以太币,你会毫不犹豫地投资。通过将规则编码到智能合约中,我们使我们的金融交易变得“去信任”。
(这个“投资”场景有点不切实际,但我会坚持使用它,因为代码简单。如果你愿意,可以想象一下点对点赌博的类似场景。)
当我在本文开头向你提供投资机会时,你拒绝了,因为你不知道我会怎么做。我会像承诺的那样付钱给你,还是会卷款逃跑?
现在我们有了一个智能合约,你可以放心接受这个提议,因为你确实知道智能合约会做什么。你可以自己阅读代码,以太坊保证了这段代码将精确执行,因为智能合约代码是不可变的。
我无法影响结果,所以你根本不需要信任我。
迟早,每个学习智能合约的开发人员都会问一个问题:“我如何修复 bug?”
许多开发人员独立提出了在运行时交换代码的方法。在过去的一年里,标准的可升级性方法变得流行起来。也许最常见的是“代理模式”。
下面是我们使用代理模式实现的可升级投资机会合约。Base
合约跟踪状态变量,Implementation
提供业务逻辑,Proxy
允许在运行时替换实现:
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);
}
}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;
}
// adapted from https://github.com/zeppelinos/labs/blob/master/upgradeability_using_eternal_storage/contracts/Proxy.sol
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) }
}
}
}
如果我将该 Proxy
合约(使用那个 Implementation
)部署到以太坊并存入 1 以太币,你会愿意投资吗?
希望你会再次拒绝。因为合约代码不再不可变,你不再知道合约会做什么。阅读代码对你没有帮助,因为我随时可以更改代码。
以下是我可能“升级”到的替代实现:
contract ExitScam is Base {
function exit() external {
require(msg.sender == owner);
selfdestruct(msg.sender);
}
}
请注意,在这个特定的例子中,你的以太币会被锁定 24 小时,因此我有充足的时间窗口来进行更改。但即使支付是即时的,我也总是可以与你的交易竞争,并尝试首先更改实现。
如果可升级性是一个 bug,该怎么办?
我的第一个建议——我意识到这在我的团队内部也是有争议的——是你可能不应该让你的合约可升级。大多数情况下,添加可升级性实际上并不会帮助你,它会破坏智能合约的价值,并且可能是 bug 的来源本身。
如果你发现可升级性是你真正的需求,考虑将合约可变性限制在不会伤害任何人的范围内。
例如,在本文描述的合约中,我可以要求代码只能在发出事件和 48 小时延迟后才能更改。因为资金只被锁定 24 小时,这将给用户留出足够的时间,如果他们不喜欢待定的代码更改,可以停止使用合约。
参数是限制合约可变性的另一种好方法。我们一开始的投资机会合约将投资者的资金翻倍。只要只增加,这个倍数就可以在运行时安全地更改。如果我更改代码以将投资增加到三倍或四倍,没有人会介意。
最后,升级智能合约最常见的方法是部署一个新合约。如果新合约对每个人来说都更好,用户会乐意停止使用旧合约并转而使用新合约。如果这种交换涉及从旧合约中读取状态,有时这被称为“迁移模式”。
更复杂的模式也存在。例如,一些合约使用代理进行升级,但要求用户选择加入后才能获得新实现。
对于开发人员来说,可升级性的诱惑是显而易见的。能够在合约部署后修复 bug 并添加功能是很棒,但这种能力本身会破坏智能合约的一个关键特性。
请记住,你的智能合约的用户不信任你,而你写智能合约的初衷就是为了这一点。
- 原文链接: medium.com/consensys-dil...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!