在本文中,将看到:1.解释这些函数解决的问题。2.讨论 Solidity 编译器如何处理新的 assert()
, require()
和 revert()
。3.给出一些经验法则来决定如何以及何时使用每一个。
取材自 Osman Rana
Crosspost:这篇文章最初是由 ConsenSys 的“Maurelian”发表的,可以在 这里找到。 这是经他许可发布的,请欣赏!
在Solidity0.4.10的版本发布引入了 assert()
、require()
和 revert()
函数,从那时起,困惑就一直存在。
特别是,assert()
和 require()
中的 “判断”函数提高了合约代码的可读性,但区分它们可能会令人困惑。
在本文中,将看到:
1.解释这些函数解决的问题。
2.讨论 Solidity 编译器如何处理新的 assert()
, require()
和 revert()
。
3.给出一些经验法则来决定如何以及何时使用每一个。
为方便起见,我使用这些功能中的每一个创建了一个简单的合约,你可以在 remix中对其进行测试。
如果你真的只想要一个太长不读版,那么以太坊stackexchange的这个答案应该可以做到。
throw
和 if ... throw 模式假设你的合约有一些特殊功能,只能由指定为 owner
的特定地址调用。
在 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
}
}
如果除owner
之外的任何人调用 useSuperPowers()
函数,该函数将抛出返回 invalid opcode
错误,撤消所有状态更改,并用完所有剩余的Gas(有关以太坊中的Gas和费用的更多信息,请参阅本文)。
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()
想象成一种礼貌的管理类型,他会指出你的错误,但更宽容。
有了那个方便的助记符,这两个函数之间的真正区别是什么?
在拜占庭网络升级之前,require()
和 assert()
实际上行为相同,但它们的字节码输出略有不同。
assert()
使用 0xfe
操作码触发错误条件require()
使用 0xfd
操作码触发错误条件如果你在黄皮书中查找其中任何一个操作码,你都不会找到它们。 这就是你看到 invalid opcode
错误的原因,因为没有关于客户端应如何处理它们的规范。
然而,在拜占庭之后,这将改变,并且在以太坊虚拟机中实现 EIP-140:REVERT 指令。 然后 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 尚不支持此返回值参数,但你可以查看此问题 以了解该更新。
目前,当你的合约抛出异常时,它会耗尽所有剩余的 gas。 这可能会导致对矿工的慷慨捐赠,并且最终会花费用户很多钱。
一旦在 EVM 中实现了 REVERT
,没有使用它来退还多余的 gas 将是明显的旧不礼貌的行为。
因此,如果revert()
和 require()
都退还任何剩余的 gas,并允许你返回一个值,为什么要使用 assert()
烧掉 gas?
区别在于字节码输出,为此我将引用文档(我这里强调):
应该使用
require
函数来确保满足有效条件,例如输入或合约状态变量,或者来自外部合约调用的有效返回值。 如果使用得当,分析工具可以评估你的合约,以确定将达到失败assert
的条件和函数调用。 正常运行的代码永远不应有失败的断言语句; 如果发生这种情况,你的合约中有一个错误,你应该修复它。
稍微澄清一下: require()
语句失败应该被认为是正常且健康的事件(与 revert()
相同)。 当 assert()
语句失败时,发生了一些非常错误和意想不到的事情,你需要修复你的代码。
通过遵循本指南,静态分析和形式验证 工具将能够检查你的合约,以找到并证明可能违反合约的条件,或证明你的合约按设计运行且没有缺陷。
在实践中,我使用一些启发式方法来帮助我决定哪个是合适的。
使用 require()
的时候:
require(input<20);
require(external.send(amount));
require(block.number > SOME_BLOCK_NUMBER)
或者 require(balance[msg.sender]>=amount)
require
在我们的智能合约最佳实践中有许多 require()
用于此类事情的最佳示例。
使用 revert()
的时候:
如果你有一些复杂的嵌套 if/else
逻辑流程,你可能会发现使用 revert()
而不是 require()
是有意义的。但请记住,复杂的逻辑是一种代码异味。
使用 assert()
的时候:
基本上, require()
应该是你检查条件的首选函数, assert()
只是为了防止发生任何非常糟糕的事情,但条件评估为 false
是不可能的。
另外:“你不应该盲目地使用 assert 进行溢出检查,但前提是你认为以前的检查(使用 if
或 require
)会使溢出变得不可能”。 ——来自@chriseth 的评论
这些功能对于你的安全工具箱来说是非常强大的工具。 知道如何以及何时使用它们不仅有助于防止漏洞,还可以使你的代码更加用户友好,并且应对未来的变化。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!