Solidity 中 revert(), assert() 和 require() 的使用方法

2024-04-04 09:58

本文主要是介绍Solidity 中 revert(), assert() 和 require() 的使用方法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

转载自:https://ethfans.org/posts/when-to-use-revert-assert-and-require-in-solidity

Solidity 0.4.10 版本发布了新的 assert() , require() 和 revert() 函数,解决了以前代码中有困惑的地方。特别地,新 assert() 和 require() 代码会“确保”提高合约代码逻辑条理清晰,但是也需要知道如何区别使用它们。

本文中,将会:

  1. 解释新函数解决的问题
  2. 讨论 Solidity 编译器如何处理新 assert() 、 require() 和 revert() 调用
  3. 给出使用新代码的最佳实践

为了更好理解,我生成了使用这些新功能的简单合约,用户可以在 remix 上进行测试。

如果只是想看“太长不看版”,那么 ethereum stackexchange 上的回答可以解答疑问。

Solidity 的错误处理模式

传统方法:采用 throw 和 if ... throw 模式

例如合约中有一些功能,只能被授权为 拥有者 的地址才能调用。

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()和require()之间的区别

首先,可以将 assert() 想象为一个过于自信的实现方式,即使有错误,也会执行并扣除gas。然而 require() 可以被想象为一个更有礼貌些的实现方式,会发现错误,并且原谅所犯错误(译注:不扣除 gas)。

基于以上理解,以上两个函数真正区别在哪里呢?在拜占庭网络更新前, require() 和 assert() 表现完全一样,但是他们的二进制代码却有略微区别。

  1. assert() 使用 0xfe 操作码引起错误条件
  2. require() 使用 0xfd 操作码引起错误条件

如果在黄皮书中查找这些操作码,会发现找不到。也就是为什么会看到 无效操作码 错误,因为并没有客户端如何处理这些错误的明确定义。

拜占庭网络升级并实现 EIP-140:以太坊虚机回滚指南之后,会解决这个问题。0xfd 操作码的改变将在 REVERT 指南中反映出来。

以下是这一激动人心功能的描述:

在0.4.10版本之后部署了许多合约,其中包括一个暂时不用的新操作代码。现在,它被激活了,就是 REVERT 。

注: throw 和 revert() 都是用 0xfd 操作码。而 0.4.10 之前,throw 就是使用的 0xfe

REVERT 操作码实现功能

REVERT 碰到无效代码后,仍将回滚所有状态,但是会用两种不同于“无效代码”方式处理:

  1. 允许返回一个数值
  2. 将剩余gas返还调用者

1. 允许返回一个数值

许多智能合约开发者对以前那种无用的无效代码错误很熟悉。幸运的是,很快新代码可以返回一个错误信息,或者代表某种错误类型的数值。

看起来像这样:

revert(‘Something bad happened’);

或者

require(condition, ‘Something bad happened’);

注:Solidity 暂时还不支持返回变量,但是可以参见这个问题更新。

2. 将剩余 gas 返还调用者

目前的合约处理 throws 后会消耗剩余的 gas。尽管可以视为对矿工的慷慨捐助,但是往往会消耗用户大量金钱。

一旦 REVERT 在 EVM 中实现,将会抛弃旧方式转而将剩余 gas 返还用户。

在revert(), assert()和require()中作出选择

那么,如果 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()

  • 检查 overflow/underflow,即:c = a+b; assert(c > b)
  • 检查非变量(invariants),即:assert(this.balance >= totalSupply);
  • 验证改变后的状态
  • 预防不应该发生的条件
  • 一般地,尽量少使用 assert 调用
  • 一般地,assert 应该在函数结尾处使用

基本上,require() 应该被用于函数中检查条件,assert() 用于预防不应该发生的情况,但不应该使条件错误。

另外,“除非认为之前的检查(用 if 或 require )会导致无法验证 overflow,否则不应该盲目使用 assert 来检查 overflow”——来自于@chriseth

结论

这些函数是安全性检查工具库中很强大的工具。知道如何以及何时使用这些函数不仅能帮助你的代码免受攻击,而且会使代码更加对用户友好,更加面向未来变化。


喜欢这种类型文章吗?

我来自ConsenSys Diligence团队。如果你有 Solidity 和 EVM 方面深入技能,而且对提高智能合约安全性很感兴趣,我们希望你能加入智能合约审查部门(从这里申请)。

如果你看到了这里但还达不到工作描述中的要求,也没关系。只需要在消息中引用此文,并说出你的技能和对以太坊的兴趣即可。

这篇关于Solidity 中 revert(), assert() 和 require() 的使用方法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/875480

相关文章

使用Python开发一个Ditto剪贴板数据导出工具

《使用Python开发一个Ditto剪贴板数据导出工具》在日常工作中,我们经常需要处理大量的剪贴板数据,下面将介绍如何使用Python的wxPython库开发一个图形化工具,实现从Ditto数据库中读... 目录前言运行结果项目需求分析技术选型核心功能实现1. Ditto数据库结构分析2. 数据库自动定位3

Python yield与yield from的简单使用方式

《Pythonyield与yieldfrom的简单使用方式》生成器通过yield定义,可在处理I/O时暂停执行并返回部分结果,待其他任务完成后继续,yieldfrom用于将一个生成器的值传递给另一... 目录python yield与yield from的使用代码结构总结Python yield与yield

Go语言使用select监听多个channel的示例详解

《Go语言使用select监听多个channel的示例详解》本文将聚焦Go并发中的一个强力工具,select,这篇文章将通过实际案例学习如何优雅地监听多个Channel,实现多任务处理、超时控制和非阻... 目录一、前言:为什么要使用select二、实战目标三、案例代码:监听两个任务结果和超时四、运行示例五

python使用Akshare与Streamlit实现股票估值分析教程(图文代码)

《python使用Akshare与Streamlit实现股票估值分析教程(图文代码)》入职测试中的一道题,要求:从Akshare下载某一个股票近十年的财务报表包括,资产负债表,利润表,现金流量表,保存... 目录一、前言二、核心知识点梳理1、Akshare数据获取2、Pandas数据处理3、Matplotl

Java使用Thumbnailator库实现图片处理与压缩功能

《Java使用Thumbnailator库实现图片处理与压缩功能》Thumbnailator是高性能Java图像处理库,支持缩放、旋转、水印添加、裁剪及格式转换,提供易用API和性能优化,适合Web应... 目录1. 图片处理库Thumbnailator介绍2. 基本和指定大小图片缩放功能2.1 图片缩放的

Python使用Tenacity一行代码实现自动重试详解

《Python使用Tenacity一行代码实现自动重试详解》tenacity是一个专为Python设计的通用重试库,它的核心理念就是用简单、清晰的方式,为任何可能失败的操作添加重试能力,下面我们就来看... 目录一切始于一个简单的 API 调用Tenacity 入门:一行代码实现优雅重试精细控制:让重试按我

Python安装Pandas库的两种方法

《Python安装Pandas库的两种方法》本文介绍了三种安装PythonPandas库的方法,通过cmd命令行安装并解决版本冲突,手动下载whl文件安装,更换国内镜像源加速下载,最后建议用pipli... 目录方法一:cmd命令行执行pip install pandas方法二:找到pandas下载库,然后

MySQL中EXISTS与IN用法使用与对比分析

《MySQL中EXISTS与IN用法使用与对比分析》在MySQL中,EXISTS和IN都用于子查询中根据另一个查询的结果来过滤主查询的记录,本文将基于工作原理、效率和应用场景进行全面对比... 目录一、基本用法详解1. IN 运算符2. EXISTS 运算符二、EXISTS 与 IN 的选择策略三、性能对比

使用Python构建智能BAT文件生成器的完美解决方案

《使用Python构建智能BAT文件生成器的完美解决方案》这篇文章主要为大家详细介绍了如何使用wxPython构建一个智能的BAT文件生成器,它不仅能够为Python脚本生成启动脚本,还提供了完整的文... 目录引言运行效果图项目背景与需求分析核心需求技术选型核心功能实现1. 数据库设计2. 界面布局设计3

使用IDEA部署Docker应用指南分享

《使用IDEA部署Docker应用指南分享》本文介绍了使用IDEA部署Docker应用的四步流程:创建Dockerfile、配置IDEADocker连接、设置运行调试环境、构建运行镜像,并强调需准备本... 目录一、创建 dockerfile 配置文件二、配置 IDEA 的 Docker 连接三、配置 Do