转载自:https://ethfans.org/posts/when-to-use-revert-assert-and-require-in-solidity
Solidity 0.4.10 版本发布了新的 assert()
, require()
和 revert()
函数,解决了以前代码中有困惑的地方。特别地,新 assert()
和 require()
代码会“确保”提高合约代码逻辑条理清晰,但是也需要知道如何区别使用它们。
本文中,将会:
assert()
、 require()
和 revert()
调用为了更好理解,我生成了使用这些新功能的简单合约,用户可以在 remix 上进行测试。
如果只是想看“太长不看版”,那么 ethereum stackexchange 上的回答可以解答疑问。
例如合约中有一些功能,只能被授权为 拥有者
的地址才能调用。
Solidity 0.4.10之前(以及其后一段时间),这种强制授权处理方式很普遍:
contract HasAnOwner {
address owner;
function useSuperPowers(){
if (msg.sender != owner) { throw; }
// do something only the owner should be allowed to do
}
}
如果 useSuperPowers()
函数被其它非拥有者调用,此函数将抛出“返回无效操作代码错误”,回滚所有状态改变,而且消耗掉剩下的gas(更多关于 gas 与费用的信息可以参考这篇 ethereum 中的文章)。
现在,“throw(抛出)”关键字已经过时了,最终将会被弃用。幸运的是,新函数 assert()
、 require()
和 revert()
提供了同样功能,而且上下文更加干净。
咱们看看用新代码函数如何处理传统 if ... throw
模式,
这行代码:
if(msg.sender != owner) { throw; }
完全等价于如下三种形式:
if(msg.sender != owner) { revert(); }
assert(msg.sender == owner);
require(msg.sender == owner);
注意在 assert()
和 require()
例子中的条件声明,是 if
例子中条件块取反,也就是用 ==
代替了 !=
。
首先,可以将 assert()
想象为一个过于自信的实现方式,即使有错误,也会执行并扣除gas。然而 require()
可以被想象为一个更有礼貌些的实现方式,会发现错误,并且原谅所犯错误(译注:不扣除 gas)。
基于以上理解,以上两个函数真正区别在哪里呢?在拜占庭网络更新前, require()
和 assert()
表现完全一样,但是他们的二进制代码却有略微区别。
assert()
使用 0xfe
操作码引起错误条件require()
使用 0xfd
操作码引起错误条件如果在黄皮书中查找这些操作码,会发现找不到。也就是为什么会看到 无效操作码
错误,因为并没有客户端如何处理这些错误的明确定义。
拜占庭网络升级并实现 EIP-140:以太坊虚机回滚指南之后,会解决这个问题。0xfd
操作码的改变将在 REVERT
指南中反映出来。
以下是这一激动人心功能的描述:
在0.4.10版本之后部署了许多合约,其中包括一个暂时不用的新操作代码。现在,它被激活了,就是 REVERT
。
注: throw
和 revert()
都是用 0xfd
操作码。而 0.4.10 之前,throw
就是使用的 0xfe
。
REVERT
碰到无效代码后,仍将回滚所有状态,但是会用两种不同于“无效代码”方式处理:
许多智能合约开发者对以前那种无用的无效代码错误很熟悉。幸运的是,很快新代码可以返回一个错误信息,或者代表某种错误类型的数值。
看起来像这样:
revert(‘Something bad happened’);
或者
require(condition, ‘Something bad happened’);
注:Solidity 暂时还不支持返回变量,但是可以参见这个问题更新。
目前的合约处理 throws 后会消耗剩余的 gas。尽管可以视为对矿工的慷慨捐助,但是往往会消耗用户大量金钱。
一旦 REVERT
在 EVM 中实现,将会抛弃旧方式转而将剩余 gas 返还用户。
那么,如果 revert()
和 require()
都会返还剩余 gas,而且允许返回一个数值,那么为什么还使用 assert()
这种会消耗 gas 的调用呢?
不同点在于输出的二进制代码,引用如下文档以便更清楚解释(我做的着重强调)
require函数用于:
- 确认有效条件,例如输入,
- 确认合约声明变量是一致的
- 从调用到外部合约返回有效值如果正确使用,分析工具会评估合约并分辨出引起
assert
调用错误的条件和函数。正确函数代码将会避免引起调用错误的 assert 声明;如果发生就意味着合约中存在需要修复的bug。
为了更清楚地解释:require()
声明失败应该被认为是正常和健壮的情况(跟 revert()
一样);而当 assert()
声明失败时,则意味着有些东西失控了,需要修复代码中的问题。
如果遵循以上实践指南,静态分析和正式验证工具可以用于检查合约,发现并证实合约中的隐患,或者确保合约安全无漏洞地运行。
特别地,我会使用如下准则来帮助判断正确使用场景。
以下场景使用 require()
:
require(input<20);
require(external.send(amount));
require(block.number > SOME_BLOCK_NUMBER)
或者 require(balance[msg.sender]>=amount)
require
函数require
应该在函数最开始的地方使用在我们的智能合约最佳实践中有很多使用 require()
的例子供参考。
以下场景使用 revert()
:
require()
同样的类型,但是需要更复杂处理逻辑的场景如果有复杂的 if/else
逻辑流,那么应该考虑使用 revert()
函数而不是require()
。记住,复杂逻辑意味着更多的代码。
以下场景使用 assert()
:
c = a+b; assert(c > b)
assert(this.balance >= totalSupply);
assert
调用assert
应该在函数结尾处使用基本上,require()
应该被用于函数中检查条件,assert()
用于预防不应该发生的情况,但不应该使条件错误。
另外,“除非认为之前的检查(用 if
或 require
)会导致无法验证 overflow
,否则不应该盲目使用 assert
来检查 overflow
”——来自于@chriseth
这些函数是安全性检查工具库中很强大的工具。知道如何以及何时使用这些函数不仅能帮助你的代码免受攻击,而且会使代码更加对用户友好,更加面向未来变化。
喜欢这种类型文章吗?
我来自ConsenSys Diligence团队。如果你有 Solidity 和 EVM 方面深入技能,而且对提高智能合约安全性很感兴趣,我们希望你能加入智能合约审查部门(从这里申请)。
如果你看到了这里但还达不到工作描述中的要求,也没关系。只需要在消息中引用此文,并说出你的技能和对以太坊的兴趣即可。