在深入理解 Solidity 错误"的第三篇, 探索处理错误,本文将揭晓这问问题的答案:asset 错误会消耗所有 gas 吗? require 提不提供错误字符有什么样的不同?外部调用的错误如何影响当前上下文?如何处理底层调用调用产生的错误?
本文是"深入理解 Solidity 错误"系列的第三篇: 如何处理错误。
在了解了错误的不同类型(编译时错误与运行时错误)、Solidity 错误的不同类型以及它们之间的区别之后,我们现在来看看处理它们的不同方法。
Solidity 提供了多种内置方法来处理错误,包括 assert()
、require()
和 revert()
。我们将在本文中了解它们之间的区别、每种方法的使用时机以及各自的优点。我们还将探讨如何处理来自外部调用(函数调用与底层调用)的错误。
最后,我们将简要介绍在编写 Solidity 时应注意的一些情况,这些情况可能会导致错误和 bug,但 Solidity 代码不会在运行时发出出错信号!
适当而准确地处理错误对任何编程语言都至关重要。我们经常听到这样的说法:
智能合约通常持有大量资金,处理重要的业务逻辑,并且一旦部署到正式主网上就无法编辑。所有这些都突显:智能合约是任务关键型程序。
因此,在以太坊和 EVM 环境中错误地处理错误更不可取。
在 solidity 0.4.x 之前,处理错误的唯一方法是使用 throw
。从 Solidity 0.8.x 开始,Solidity 有 4 种不同的错误处理方式:
revert
assert
require
invalid
在 Solidity 开发者社区中,经常会有这样的困惑:是否应该在合约中使用assert()
;如果应该使用,那么是何时以及在何种情况下使用呢。
Consensys在他们的智能合约最佳实践指南中为这个问题提供了最直接的答案:
使用 assert()
强化不变性
不变性的意思是"在执行过程中假定永远为真的东西 "。换句话说,不变性是指在合约部署后的整个生命周期内都不应该改变且始终保持不变的属性。(例如:代币发行合约中代币与以太币的发行比例可能是固定的)。
Solidity 文档也同样并建议在以下情况下使用 assert()
语句:
Assert 保护有助于验证这一点在任何时候都是正确的。
assert()
可以通过检查条件来使用。如果不满足条件,assert()
将:
Panic(uint256)
的错误。条件被指定为 assert()
的第一个参数,其必须是布尔值 true
或 false
。如果布尔条件的值为 false
则会产生异常。
assert(bool condition)
与 require()
不同,你不能提供错误提示字符串作为 assert()
的第二个参数。
在 Solidity 0.8.0 之前,assert()
会消耗所有提供给交易的Gas。自 Solidity 0.8.0 版本发布后,情况不再如此。
在 0.7.6 之前,当 assert()
中的条件失败时:
自 0.8.0 起,当 assert()
中的条件失败时:
这是因为在 0.8.0 之前,Panic(uint256)
类型的错误在 EVM 的底层使用了 INVALID
操作码。现在情况不再如此。自 0.8.0 版 Solidity 起,Panic(uint256)
使用 REVERT
操作码。
Solidity 0.8.0 突破性更改 (来源:Solidity 文档)
让我们来看一个基本示例。如果将基本代码片段粘贴到 Remix 中,并将数字 0
作为函数 addToNumber(...)
的输入,就会违反assert
条件。
contract Assert {
uint256 number;
function addToNumber(uint256 input) public {
uint256 before = number;
number += input;
assert(number > before);
}
}
提供 3 百万Gas,我们可以看到不同的错误信息
<p align="center">自 Solidity 0.8.0 版起,assert 不再消耗所有Gas</p>
注意:若要消耗所有Gas,可通过内联汇编使用 INVALID
操作码来强制消耗调用中可用的所有剩余Gas。更多详情,请参阅下文有关 invalid
的部分。
作为一个 Solidity 开发者,一旦你开始掌握 "智能合约不变性 "和不变属性的概念,assert()
就能帮助你加强智能合约的安全性。
遵循这一范例,形式分析工具就能验证智能合约的不变性是否被违反,并使智能合约永远无法达到某些属性被更改的状态。
这意味着不会违反代码中的不变性,而且代码经过了形式验证。
在 Solidity 代码中使用 assert()
时,可以运行 SMT Checker 或 K-Framework 等形式化验证工具,查找可能违反这些属性的方法和调用路径。这将有助于找到更多的攻击向量和漏洞,从而加强合约的安全性。
断言防护通常应与其他技术相结合,例如暂停合约并允许升级。
否则,你最终可能会被一个总是失败的断言困住。
你可以在智能合约 (SWC-110) 中看到一些违反断言的好例子。
注意: 在 Metropolis 发布之前,使用
require
的异常会消耗所有Gas。现在情况已不同。
在 Solidity 中,require()
语句是最常用的错误处理方式之一(尽管由于通过revert
自定义错误的使用越来越多,require
的使用也在慢慢减少)。
顾名思义,require
有助于确保在智能合约中执行某些函数时满足某些条件(运行时所需的条件)。
根据 Consensys 智能合约最佳实践,require()用于确保满足有效条件,如输入或合约状态变量,或验证调用外部合约的返回值。
require
可以创建:
Error(string)
类型的错误我们将在接下来的章节中详细了解。
require()
的用法与 "assert"相同,用于检查条件。如果不符合条件,就会抛出异常。
条件为 require()
的第一个参数,其值为 true
或 false
。
如果不满足条件,则抛出异常:
Error(string)
的错误。require()
可以选择是后指定错误字符串:
require(bool condition)
require(bool condition, string memory message)
也可以:
require()
检查中使用 &&
( 位与运算符)组合检查多个条件||
检查一个或另一个条件是否有效下面是 UniswapFactory V3 中的一个示例,说明如何通过 &&
在 require
中同时确保两个条件。
应该使用 require()
检查来确保满足条件,只有在调用已部署的合约并与之进行 实时
交互(无论是在开发测试网还是主网)时才能检测条件是否满足。我们称之为 运行时
,即执行合约代码时。
必须检查的条件和需要验证的输入包括:
下面是从 OpenZeppelin 的 ERC20
代币合约的 Solidity 代码中提取的一个常用示例。我们可以从下面的截图中看到,函数上方注释中的要求是通过 Solidity 代码中的 require(...)
语法检查的。
关于检查外部调用返回的值,OpenZeppelin 的 Address
库在使用库中的 sendValue(...)
时会检查底层调用是否返回了 success
布尔值。
来源:https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol
使用 require()
时,你可以选择提供一个错误提示字符串作为第二个参数。其形式为 require(condition, "error message")
, 此时将创建一个 Error(string) 类型的错误。
当你提供错误提示作为 require(condition, "error message")
的第二个参数时,error message
将是一个 abi 编码的 字符串
,就像调用名为 Error(string)
的函数一样。不过没有函数被调用;只是使用Error(string)
的字节4选择器来区分错误类型。
function Error(string memory) public {
// ...
}
让我们举例说明,请看下面的代码片段:
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
以十六进制返回的错误数据格式如下:
0x08c379a0 // Error(string) 的函数选择器
0x0000000000000000000000000000000000000000000000000000000000000020 // 数据偏移
0x000000000000000000000000000000000000000000000000000000000000001a // 数据长度
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // 字符串数据 (utf8 encoded hex "Not enough Ether provided.")
让我们来详细分析一下:
0x08c379a0
= Error(string)
的 keccak256 哈希值的前...如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!