深入理解 Solidity 错误 #3 - 错误处理

  • Tiny熊
  • 更新于 2023-08-03 16:59
  • 阅读 1572

在深入理解 Solidity 错误"的第三篇, 探索处理错误,本文将揭晓这问问题的答案:asset 错误会消耗所有 gas 吗? require 提不提供错误字符有什么样的不同?外部调用的错误如何影响当前上下文?如何处理底层调用调用产生的错误?

本文是"深入理解 Solidity 错误"系列的第三篇: 如何处理错误。

在了解了错误的不同类型(编译时错误运行时错误)、Solidity 错误的不同类型以及它们之间的区别之后,我们现在来看看处理它们的不同方法。

Solidity 提供了多种内置方法来处理错误,包括 assert()require()revert()。我们将在本文中了解它们之间的区别、每种方法的使用时机以及各自的优点。我们还将探讨如何处理来自外部调用(函数调用与底层调用)的错误。

最后,我们将简要介绍在编写 Solidity 时应注意的一些情况,这些情况可能会导致错误和 bug,但 Solidity 代码不会在运行时发出出错信号!

关于 Solidity 中的错误处理

适当而准确地处理错误对任何编程语言都至关重要。我们经常听到这样的说法:

  • 在编写 try 代码块之前先编写 catch 代码块
  • "及早抛出错误"

智能合约通常持有大量资金,处理重要的业务逻辑,并且一旦部署到正式主网上就无法编辑。所有这些都突显:智能合约是任务关键型程序。

因此,在以太坊和 EVM 环境中错误地处理错误更不可取。

在 solidity 0.4.x 之前,处理错误的唯一方法是使用 throw。从 Solidity 0.8.x 开始,Solidity 有 4 种不同的错误处理方式:

  • 使用 revert
  • 使用 assert
  • 使用 require
  • 使用invalid

Assert()

在 Solidity 开发者社区中,经常会有这样的困惑:是否应该在合约中使用assert();如果应该使用,那么是何时以及在何种情况下使用呢。

Consensys在他们的智能合约最佳实践指南中为这个问题提供了最直接的答案:

使用 assert() 强化不变性

不变性的意思是"在执行过程中假定永远为真的东西 "。换句话说,不变性是指在合约部署后的整个生命周期内都不应该改变且始终保持不变的属性。(例如:代币发行合约中代币与以太币的发行比例可能是固定的)。

Solidity 文档也同样并建议在以下情况下使用 assert() 语句:

  • 测试内部错误
  • 检查不变性

Assert 保护有助于验证这一点在任何时候都是正确的。

如何在 Solidity 中使用 assert()?

assert() 可以通过检查条件来使用。如果不满足条件,assert() 将:

  • 抛出一个类型为 Panic(uint256) 的错误。
  • 还原所有状态更改。

条件被指定为 assert() 的第一个参数,其必须是布尔值 truefalse。如果布尔条件的值为 false 则会产生异常。

assert(bool condition)

require() 不同,你不能提供错误提示字符串作为 assert() 的第二个参数。

自 Solidity 0.8.0 起 assert 行为有变化

在 Solidity 0.8.0 之前,assert() 会消耗所有提供给交易的Gas。自 Solidity 0.8.0 版本发布后,情况不再如此。

在 0.7.6 之前,当 assert() 中的条件失败时:

  • 所有状态变化都将回滚。
  • 所有提供的Gas将被消耗。

自 0.8.0 起,当 assert() 中的条件失败时:

  • 所有状态变化都会回滚。
  • 剩余的Gas将返回给发起者。

这是因为在 0.8.0 之前,Panic(uint256)类型的错误在 EVM 的底层使用了 INVALID操作码。现在情况不再如此。自 0.8.0 版 Solidity 起,Panic(uint256) 使用 REVERT 操作码。

solidity- 错误处理 solidity 0.8 asset

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,我们可以看到不同的错误信息

solidity- 错误处理 solidity 0.8 asset

<p align="center">自 Solidity 0.8.0 版起,assert 不再消耗所有Gas</p>

注意:若要消耗所有Gas,可通过内联汇编使用 INVALID 操作码来强制消耗调用中可用的所有剩余Gas。更多详情,请参阅下文有关 invalid的部分。

使用 assert() 进行形式验证

作为一个 Solidity 开发者,一旦你开始掌握 "智能合约不变性 "和不变属性的概念,assert()就能帮助你加强智能合约的安全性。

遵循这一范例,形式分析工具就能验证智能合约的不变性是否被违反,并使智能合约永远无法达到某些属性被更改的状态。

这意味着不会违反代码中的不变性,而且代码经过了形式验证。

在 Solidity 代码中使用 assert()时,可以运行 SMT Checker 或 K-Framework 等形式化验证工具,查找可能违反这些属性的方法和调用路径。这将有助于找到更多的攻击向量和漏洞,从而加强合约的安全性。

关于 assert() 的最后重要说明

断言防护通常应与其他技术相结合,例如暂停合约并允许升级。

否则,你最终可能会被一个总是失败的断言困住。

你可以在智能合约 (SWC-110) 中看到一些违反断言的好例子。

require()

注意: 在 Metropolis 发布之前,使用 require 的异常会消耗所有Gas。现在情况已不同。

在 Solidity 中,require()语句是最常用的错误处理方式之一(尽管由于通过revert自定义错误的使用越来越多,require 的使用也在慢慢减少)。

顾名思义,require 有助于确保在智能合约中执行某些函数时满足某些条件(运行时所需的条件)。

根据 Consensys 智能合约最佳实践,require()用于确保满足有效条件,如输入或合约状态变量,或验证调用外部合约的返回值。

require可以创建:

  • 没有数据的错误
  • Error(string) 类型的错误

我们将在接下来的章节中详细了解。

如何使用 require()?

require() 的用法与 "assert"相同,用于检查条件。如果不符合条件,就会抛出异常。

条件为 require() 的第一个参数,其值为 truefalse

如果不满足条件,则抛出异常:

  • 则抛出类型为 Error(string) 的错误。
  • 所有状态更改都会回滚
  • 未使用的 Gas 返回给交易发起者

require() 可以选择是后指定错误字符串:

require(bool condition)
require(bool condition, string memory message)

使用 require() 检查多个条件(或至少一个条件)

也可以:

  • 在一个 require() 检查中使用 && ( 位与运算符)组合检查多个条件
  • 使用 || 检查一个或另一个条件是否有效

下面是 UniswapFactory V3 中的一个示例,说明如何通过 &&require 中同时确保两个条件。

solidity- 错误处理 require 检查多个条件

来源:https://github.com/Uniswap/v3-core/blob/e3589b192d0be27e100cd0daaf6c97204fdb1899/contracts/UniswapV3Factory.sol#L61-L72

何时使用 require()?

应该使用 require() 检查来确保满足条件,只有在调用已部署的合约并与之进行 实时交互(无论是在开发测试网还是主网)时才能检测条件是否满足。我们称之为 运行时,即执行合约代码时。

必须检查的条件和需要验证的输入包括:

  • 提供给函数参数的输入。
  • 从外部调用其他合约的返回值。
  • 处理后必须具有特定值的合约状态。

下面是从 OpenZeppelin 的 ERC20代币合约的 Solidity 代码中提取的一个常用示例。我们可以从下面的截图中看到,函数上方注释中的要求是通过 Solidity 代码中的 require(...) 语法检查的。

solidity- 错误处理

关于检查外部调用返回的值,OpenZeppelin 的 Address 库在使用库中的 sendValue(...) 时会检查底层调用是否返回了 success 布尔值。

solidity- call 错误处理

来源:https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol

require() 带错误提示

使用 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 &lt;= 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 哈希值的前...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Tiny熊
Tiny熊
0xD682...E8AB
登链社区发起人 通过区块链技术让世界变得更好而尽一份力。