我们倡导“时刻准备失败",提前知道你的代码是否安全是不可能的。然而,我们可以允许合约以可预知的方式失败,然后最小化失败带来的损失。本章将带你了解如何为可预知的失败做准备。
注意:当你向你的系统添加新的组件时总是伴随着风险的。一个不良设计本身会成为漏洞-一些精心设计的组件在交互过程中同样会出现漏洞。仔细考虑你在合约里使用的每一项技术,以及如何将它们整合共同创建一个稳定可靠的系统。
升级有问题的合约
如果代码中发现了错误或者需要对某些部分做改进都需要更改代码。在以太坊上发现一个错误却没有办法处理他们是太多意义的。
关于如何在以太坊上设计一个合约升级系统是一个正处于积极研究的领域,在这篇文章当中我们没法覆盖所有复杂的领域。然而,这里有两个通用的基本方法。最简单的是专门设计一个注册合约,在注册合约中保存最新版合约的地址。对于合约使用者来说更能实现无缝衔接的方法是设计一个合约,使用它转发调用请求和数据到最新版的合约。
无论采用何种技术,组件之间都要进行模块化和良好的分离,由此代码的更改才不会破坏原有的功能,造成孤儿数据,或者带来巨大的成本。 尤其是将复杂的逻辑与数据存储分开,这样你在使用更改后的功能时不必重新创建所有数据。
当需要多方参与决定升级代码的方式也是至关重要的。根据你的合约,升级代码可能会需要通过单个或多个受信任方参与投票决定。如果这个过程会持续很长时间,你就必须要考虑是否要换成一种更加高效的方式以防止遭受到攻击,例如紧急停止或断路器。
Example 1:使用注册合约存储合约的最新版本
在这个例子中,调用没有被转发,因此用户必须每次在交互之前都先获取最新的合约地址。
contract SomeRegister { address backendContract; address[] previousBackends; address owner;
function SomeRegister() { owner = msg.sender; }
modifier onlyOwner() {
if (msg.sender != owner) { throw; } _; }
function changeBackend(address newBackend) public
onlyOwner() returns (bool) { if(newBackend != backendContract)
previousBackends.push(backendContract); backendContract = newBackend; return true; }
return false; } }
这种方法有两个主要的缺点:
用户必须始终查找当前合约地址,否则任何未执行此操作的人都可能会使用旧版本的合约
在你替换了合约后你需要仔细考虑如何处理原合约中的数据
另外一种方法是设计一个用来转发调用请求和数据到最新版的合约:
Example 2:使用DELEGATECALL** 转发数据和调用**
contract Relay { address public currentVersion; address public owner;
modifier onlyOwner() { if (msg.sender != owner) { throw; } _; }
function Relay(address initAddr) { currentVersion = initAddr; owner = msg.sender; // this owner may be another contract with multisig, not a single contract owner }
function changeContract(address newVersion) public onlyOwner() { currentVersion = newVersion; }
function() { if(!currentVersion.delegatecall(msg.data)) throw; } }
这种方法避免了先前的问题,但也有自己的问题。它使得你必须在合约里小心的存储数据。如果新的合约和先前的合约有不同的存储层,你的数据可能会被破坏。另外,这个例子中的模式没法从函数里返回值,只负责转发它们,由此限制了它的适用性。(这里有一个更复杂的实现 想通过内联汇编和返回大小的注册表来解决这个问题)
无论你的方法如何,重要的是要有一些方法来升级你的合约,否则当被发现不可避免的错误时合约将没法使用。
断路器(暂停合约功能)