Solidity 0.8.0 是 Solidity 编译器和语言的一个重大版本发布。
此版本的一些新特性已在0.8.x 预览版本发布文章中进行了详细说明。请将预览版本的二进制文件视为已失效,请勿再使用。
值得注意的新特性和更改
像往常一样,此重大版本不包含许多新特性,而是包含一些需要在语法或语义上进行向后不兼容调整的更改。有关详细说明,请参阅文档。
将影响大多数用户的更改是,算术运算现在默认情况下会进行检查,这意味着溢出和下溢将导致回退。此功能可以通过使用unchecked 块在本地禁用。
第二个非常明显的更改是 ABI 编码器 v2 默认情况下处于激活状态。您可以使用 pragma abicoder v1 激活旧的编码器,或使用 pragma abicoder v2 显式选择 v2 - 这与 pragma experimental ABIEncoderV2 的效果相同。ABI 编码器 v2 比 v1 更复杂,但它还会对输入执行额外的检查,并支持比 v1 更广泛的类型集。
此外,诸如除以零、断言失败等内部错误不再使用无效操作码,而是使用 revert 并附带特殊的错误消息,以避免在这些情况下浪费 gas。
另一个重要的更改是,我们限制了显式转换的可能性以避免歧义。之前所有可能的转换仍然可能,但您可能需要执行两次转换才能达到目标 - 尽管这不会影响生成的代码。
这些只是 0.8.0 中一些重要的重大更改,请参阅下面的更改日志以获取完整列表!
检查算术
Solidity 0.8.x 的“检查算术”特性包含三个子特性
- 在断言失败和类似条件下回退,而不是使用无效操作码。
- 检查算术,即在溢出、下溢等情况下回退。
- unchecked 块。
在断言失败和类似条件下回退,而不是使用无效操作码
以前,诸如除以零、断言失败、数组越界访问等内部错误会导致执行无效操作码。其目的是将这些更关键的错误与非关键错误区分开来,非关键错误超出了智能合约程序员的范围,例如无效输入、余额不足以进行代币转账等。这些非关键错误使用 revert 操作码,并可选地添加错误描述。
无效操作码的问题在于,与 revert 操作码相反,它会消耗所有可用的 gas。这使得它非常昂贵,您应该不惜一切代价避免它。
我们希望将算术溢出视为关键错误,但又不想让它消耗所有 gas。作为折衷方案,我们选择使用 revert 操作码,但提供不同的错误数据,以便静态(和动态)分析工具可以轻松地将它们区分开来。
更具体地说
- 非关键错误将使用空错误数据或与对具有 Error(string) 签名函数的函数调用的 ABI 编码相对应的错误数据进行回退。
- 关键错误将使用与对具有 Panic(uint256) 签名函数的函数调用的 ABI 编码相对应的错误数据进行回退。
因此,我们也使用术语“panic”来表示此类关键错误。
为了区分这两种情况,您可以在发生错误时查看返回数据的头四个字节。如果它是 0x4e487b71,则表示 panic,如果它是 0x08c379a0 或消息为空,则表示“常规”错误。
Panic 使用特定的错误代码来区分触发 panic 的某些情况。这是当前的错误代码列表
- 0x01:如果您使用计算结果为 false 的参数调用 assert。
- 0x11:如果算术运算导致在 unchecked { ... } 块之外发生下溢或溢出。
- 0x12:如果您除以零或对零取模(例如 5 / 0 或 23 % 0)。
- 0x21:如果您将过大或负的值转换为枚举类型。
- 0x22:如果您访问编码不正确的存储字节数组。
- 0x31:如果您在空数组上调用 .pop()。
- 0x32:如果您在超出范围或负索引处访问数组、bytesN 或数组切片(即 x[i],其中 i >= x.length 或 i < 0)。
- 0x41:如果您分配了太多内存或创建了过大的数组。
- 0x51:如果您调用内部函数类型的零初始化变量。
此代码列表将来可能会扩展。
请注意,您目前无法使用 try ... catch Panic(uint _code) { ... } 来捕获 panic,但这计划在不久的将来实现。
检查算术,即在溢出、下溢等情况下回退。
默认情况下,所有算术运算都将执行溢出和下溢检查(Solidity 已经具有除以零检查)。如果发生下溢或溢出,将抛出 Panic(0x11) 错误,并且调用将回退。
例如,以下代码将触发此类错误
contract C { function f() public pure { uint x = 0; x--; } }
这些检查基于变量的实际类型,因此即使结果适合 256 位的 EVM 字,如果您的类型是 uint8 且结果大于 255,它也会触发 Panic。
我们没有对从较大类型到较小类型的显式类型转换引入检查,因为我们认为此类检查可能会出乎意料。我们很乐意收到您对此事的意见!
以下操作(包括它们的复合赋值版本,例如 +=)现在具有之前没有的检查
- 加法 (+)、减法 (-)、乘法 (*)
- 增量和减量 (++ / --)
- 一元否定 (-)
- 幂运算 (**)
- 除法(见下文)(/)
有一些您可能意想不到的极端情况。例如,一元否定可以在有符号类型上触发 panic。问题在于,在二进制补码表示中,对于每个位宽,负数的数量比正数的数量多一个。这意味着以下代码将触发 panic
function f() pure { int8 x = -128; -x; }
原因是 128 不适合 8 位有符号整数。
出于同样的原因,有符号除法可能导致 panic
function f() pure { int8 x = -128; x/(-1); }
每个想要为幂运算编写 SafeMath 函数的人可能都注意到这样做相当昂贵,因为 EVM 没有提供溢出信号。本质上,您必须在不使用 exp 操作码的情况下实现自己的 exp 例程以应对一般情况。
我们希望我们找到了一个相当有效的实现,并且也感谢您对此的反馈!
对于许多特殊情况,我们实际上使用 exp 操作码而不是我们自己的实现来实现它。更具体地说,使用字面量数字作为底数的幂运算将直接使用 exp 操作码。对于值较小的变量作为底数,也存在专门的代码路径。对于最多 306 的底数,如果指数小于硬编码的安全上限,它将直接使用 exp 操作码。如果底数或指数过大,它可能会回退到基于循环的实现。
unchecked 块
由于检查算术使用更多 gas,如上一节所述,并且因为有时您确实希望包装行为,例如在实现加密例程时,我们提供了一种禁用检查算术并重新启用以前的“包装”或“模”算术的方法
表单 unchecked { ... } 的块内的所有操作都使用包装算术。您可以将此特殊块用作另一个块内的常规语句
contract C { function f() public pure returns (uint) { uint x = 0; unchecked { x--; } return x; } }
unchecked 块可以包含任意数量或任何类型的语句,并且会创建自己的变量作用域。如果 unchecked 块中包含函数调用,则函数不会继承该设置 - 如果您也希望它使用包装算术,则必须在函数内部使用另一个 unchecked 块。
我们决定对 unchecked 块进行一些限制
-
在修饰符中,您不能在 unchecked 块内使用 _;,因为这可能会导致对属性是否被继承产生混淆。
-
您只能在块内使用它,而不能用它替换块。例如,以下代码片段均无效
function f() unchecked { uint x = 7; }
function f() pure { uint x; for (uint i = 0; i < 100; i++) unchecked { x += i; } }
您必须将 unchecked 块包装在另一个块中才能使其工作。
显式转换
只有当符号、宽度、可支付性或类型类别(int、address、bytesNN 等)最多发生一次更改时,显式转换才可能。
例如,将 int8 转换为 uint16 只有在您通过 uint8 或 int16 进行转换时才可能。由于中间转换的选择会对结果产生影响,因此我们希望将其明确化
- uint16(int16(int8(-1))) 的结果为 0xffff,而
- uint16(uint8(int8(-1))) 为 0xff。
在可支付地址、字面量、枚举和合约方面还有更多此类更改。有关更改的完整列表,请参阅文档。
一般说明
由于我们通常不向后移植错误修复,因此建议将所有代码升级为与 Solidity v0.8.0 兼容。
您可以在此处找到有关如何更新代码的指南。
请注意,下面列出的更改是**0.7.6 和 0.8.0 之间的更改**。对于在 0.7.x 系列期间引入的更改,请参阅此博客上的各个更改日志或版本发布公告。
完整更改日志
重大更改
- 代码生成器:默认情况下会检查所有算术运算。可以使用 unchecked { ... } 禁用这些检查。
- 代码生成器:如果访问存储中的字节数组且其长度编码不正确,则导致恐慌。
- 代码生成器:在断言失败时,使用错误签名 Panic(uint256) 和错误代码的 revert,而不是无效操作码。
- 命令行界面:JSON 字段 abi、devdoc、userdoc 和 storage-layout 现在是子对象而不是字符串。
- 命令行界面:删除 --old-reporter 选项。
- 命令行界面:删除旧版 --ast-json 选项。现在仅支持 --ast-compact-json 选项。
- 通用:默认启用 ABI 编码器 v2。
- 通用:删除全局函数 log0、log1、log2、log3 和 log4。
- 解析器:指数运算符是右结合的。 a**b**c 被解析为 a**(b**c)。
- 扫描器:删除对 \b、\f 和 \v 转义序列的支持。
- 标准 JSON:删除 legacyAST 选项。
- 类型检查器:函数调用选项只能给出一次。
- 类型系统:不允许使用名称为 this、super 和 _ 的声明,公共函数和事件除外。
- 类型系统:不允许在 receive() 函数中使用 msg.data。
- 类型系统:不允许 type(super)。
- 类型系统:不允许枚举具有超过 256 个成员。
- 类型系统:不允许从负字面量和大于 type(uint160).max 的字面量到 address 类型的显式转换。
- 类型系统:不允许 byte 类型。它曾是 bytes1 的别名。
- 类型系统:显式转换为 address 类型始终返回非可支付的 address 类型。特别是,address(u)、address(b)、address(c) 和 address(this) 的类型为 address 而不是 address payable(这里 u、b 和 c 是类型分别为 uint160、bytes20 和合约类型的任意变量)。
- 类型系统:如果两种类型之间的显式转换同时更改了符号、宽度或种类中的多个,则不允许。
- 类型系统:仅当值适合枚举时,才允许从字面量到枚举的显式转换。
- 类型系统:从字面量到整数类型的显式转换与隐式转换一样严格。
- 类型系统:引入 address(...).code 以检索代码作为 bytes memory。可以通过 address(...).code.length 获取大小,但目前它始终包括复制代码。
- 类型系统:引入 block.chainid 用于检索当前链 ID。
- 类型系统:支持 address(...).codehash 用于检索帐户的代码哈希。
- 类型系统:全局变量 tx.origin 和 msg.sender 的类型为 address 而不是 address payable。
- 类型系统:一元否定运算符只能用于带符号整数,不能用于无符号整数。
- 视图纯检查器:将 chainid 标记为视图。
- Yul:不允许使用保留标识符,例如 EVM 指令,即使它们在给定的方言/EVM 版本中不可用。
- Yul:"带对象的 EVM" 方言中的 assignimmutable 内置函数将要修改的代码的基本偏移量作为附加参数。
语言特性
- 现在可以使用成员表示法调用超级构造函数,例如 M.C(123)。
错误修复
- 类型检查器:在数组长度表达式中使用常量时,执行正确的截断整数算术运算。
AST 更改
- 新的 AST 节点 IdentifierPath 在许多地方替换了 UserDefinedTypeName。
- 新的 AST 节点 UncheckedBlock 用于 unchecked { ... }。
非常感谢所有帮助使此版本发布成为可能的贡献者!
从此处下载新版本的 Solidity 这里。