本文为智能合约设计模式系列的一部分。
目的
保证智能合约的行为和入参符合预期
动机
就像法律合约一样,合约生效需要满足一些前提。例如,遗嘱生效的前期是遗嘱人去世。生活中有律师和司法人员来确保前提满足,在区块链世界,智能合约也需要守卫检查确保合约确实被触发。
智能合约的行为需要先检查所有前提,只有当一切都前提都符合时才运行。一旦发生问题,合同将撤销所有操作。Solidity利用了EVM错误处理方式实现这一点:发生错误时,所有的更改都会被还原,整个事务不会产生任何效果。Solidity使用异常来触发错误处理并回滚状态。Solidity提供了几种触发异常的方式。这个模式描述了它们的差异,并给出了如何以及何时使用它们。
适用性
在以下条件时使用守卫检查模式
- 验证用户输入
- 运行前检查合约状态
- 检查合约中的不变量
- 排查不可能的条件
参与者和协作
虽然此模式可用于验证用户提交的数据或从其他合约返回的数据,但唯一的参与者是主合约本身,因为所有行为都是其内部发生。
实现
在Solidity 0.4.10之前,通常使用if子句检查并抛出异常:if(testator != deceased) { throw; }
。 从0.4.13开始,关键字throw
被弃用,建议使用revert()
, require()
和 assert()
。本节将介绍如何以及何时使用它们。
拜占庭更新之后,require()
和assert()
行为有所不同。require()
和revert()
操作码是0xfd
(REVERT
),assert()
操作码是0xfe
(INVALID
)。它们最大的区别是gas消耗。REVERT
返回抛出异常时为花费的gas,INVALID
将耗尽所有提供的gas。
Solidity文档建议使用require
来确保有效的条件,例如输入或合约状态,或验证外部合约调用的返回值,而assert
只用于测试内部错误和检查不变量。两种方法都验证传入的布尔参数是否为false
,如果为false
,则抛出异常。revert
直接抛出异常。因此,它在复杂的情况下非常有用,例如if-else树,在这种情况下,条件很难作为参数传入require
。
一般来说,require
应该用于函数的开头进行验证,并且应该比其它两个更经常使用。assert
方法在函数末尾使用,只应防止严重错误。正常情况下,如果没有bug,assert
语句结果不应为true
。
从0.4.22开始,这些方法增加错误信息参数,require(bool condition, string message)
和 revert(string message)
代码示例
示例合同是一个捐赠分销商。用户将他们想要捐赠的地址以及捐赠的以太币发给合约。如果受赠地址上余额为0,则会转发全部捐款。如果受赠地址余额大于0,但是少于捐款地址余额,那么一半的捐款将被转发,而另一半将留在合同中,以备将来发放(示例没有实现)。如果受赠地址资金多于捐赠地址,不应捐赠任何以太币。这个示例合同展示了守卫检查模式的三种方法。
// This code has not been professionally audited, therefore I cannot make any promises about
// safety or correctness. Use at own risk.
contract GuardCheck {
function donate(address addr) payable public {
require(addr != address(0));
require(msg.value != 0);
uint balanceBeforeTransfer = this.balance;
uint transferAmount;
if (addr.balance == 0) {
transferAmount = msg.value;
} else if (addr.balance < msg.sender.balance) {
transferAmount = msg.value / 2;
} else {
revert();
}
addr.transfer(transferAmount);
assert(this.balance == balanceBeforeTransfer - transferAmount);
}
}
第6行require
语句确保用户提供的受赠地址不为零,如果用户忘记指定受赠地址,则为零。第7行检查用户是否在交易中附加了捐赠额,如果捐赠额为0,合约结束。第11行开始的if-else块根据受赠机构当前余额确定要发送给它的金额。如果受赠地址比捐赠地址拥有更多的余额,那么第16行中的revert
将确保没有资金转移,调用被恢复。第19行发送捐款金额到受赠地址。第20行的assert
语句确保发送后的合约余额等于捐赠前的余额减去捐赠金额。正常情况下,它永远为真。如果断言不为真,整个交易,包括捐款转移到受赠地址将被恢复。
后果
守卫检查模式的一个好处是提高可读性。同if/throw结构相比,使用require
函数更容易让非软件专业的读者理解操作意图。此外,新表达式更简洁。分成不同的方法也利于各自以后的扩展。如前所述,revert
和require
方法支持错误信息,而assert
可用于评估结合静态分析和正式验证等技术,以识别破坏合约逻辑的情况。三种函数适合不同场景的应用,为开发者提供了灵活性。
对于没有使用此模式经验的用户来说,三种方法会导致困惑,因为它们名称类似,但没有任何不同的解释。使用错误的方法会导致不希望的行为,例如,丢失所有的gas由于参数的输入错误。
守卫检查提供了一种可靠的方法来处理错误并防止异常行为,它是访问限制模式的一个重要组成部分。
已知应用
这种模式可以应用在几乎每一个已发布的合约中。一个很好的例子是HODLit合约,一种激励持有以太币的代币,它包含所有三种方法。require
表达式用于方法开头的检查,assert
用于确保算术运算不能溢出或下溢。在第269行的fallback函数中调用revert
方法,以避免用户误发送以太币给合约。
这个casino contract是个负面例子。合约中每个检查都使用assert
。其中任何一个检查失败,将导致所有提供的gas损失。如果用户希望确保交易执行,而提供非常高的gas limit,可能导致损失大量gas。
推而广之
智能合约由于其公开透明去中心化的特质,导致安全成为影响它的重要问题之一。公开透明意味着源代码甚至合约状态谁都可以查看。去中心化意味着谁都可以调用合约的方法。以太坊、EOS等公链的匿名性更为黑客提供了肆无忌惮攻击的前期。联盟链提供的身份机制,从一定程度上缓解了安全问题(事后追查),但目前联盟链并不能设置方法级别的访问权限,也就是说无法阻止联盟中用户调用合约方法,况且有些方法从业务上就需要多个角色调用。因此对方法调用的安全保护成为智能合约开发的头等大事。
守卫检查模式着重于方法参数、方法返回等方面的安全。
访问限制模式着重于访问权限、合约状态等方面的安全。
本模式建议智能合约方法参考契约式编程,设计好每个方法的前置条件,并在方法开始务必确保前置条件为真。对于某些重要的方法,还要考虑后置条件,并在方法返回前检查后置条件。
每种链都会提供类似Solidity的检查方法。EOS有eosio_assert(bool, string)
, 如果第一个参数检查条件为false
,就会返回第二个参数指定的错误信息,同时本次调用不消耗资源,这也鼓励了合约中多增加检查语句。HyperLedger Fabric智能合约一般采用Go语言开发,如果检查失败,调用shim.Error(string)
返回含有指定信息的错误,联盟链取消了资源收费,更不必担心检查成本。
完整内容请查看智能合约设计模式系列