随着 Solidity 0.8.x 系列即将发布,我们想让大家了解即将发布的重大变更。
我们想提供一个预览版的二进制文件供大家尝试,以便您能给我们反馈。
0.8.x 的主要变化是默认情况下切换到检查算术运算。这意味着x + y 在溢出时会抛出异常。换句话说:您不再需要 SafeMath 了!
由于 0.8.x 的范围尚未完全确定,您仍然有机会影响将会包含的内容和方式!您可以创建一个新问题 或者在 现有问题 上发表您的意见。
目前暂定接受的更改是我们 项目看板 中 “实现积压” 列中标记为 “重大变更” 的那些。
您已经可以浏览 更新后的文档,它应该与编译器的行为一致。如果您发现任何错误,请打开一个问题!
0.8.x 预览版二进制文件!
您可以在以下位置找到预览版二进制文件
请注意,所有这些编译器都会显示有关它们是预发布版本的警告,这是故意的。
如果您使用 solc-js,只需将二进制文件复制为 soljson.js 到 solc-js npm 模块的根目录。
您也可以在 Remix 中试用 0.8.x 编译器。只需选择左侧的 “Solidity 编译器” 选项卡即可切换到编译器侧边栏,然后点击编译器列表下拉框上方的不起眼的 + 图标。将 soljson.js URL 粘贴到弹出的对话框中的 “URL” 字段,然后点击 “确定”。您现在应该看到 custom 作为选定版本,并且能够使用它来编译您的代码。如果遇到问题,请参考 Remix 文档关于编译器模块。
确保使用 pragma solidity >0.7.0 - 否则 Remix 可能会切换回其他版本。
对于其他编译 Solidity 合约的方式,请将二进制文件复制到相应的位置。
检查算术
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:如果您将一个过大或负的值转换为枚举类型。
- 0x31:如果您在空数组上调用 .pop()。
- 0x32:如果您在越界或负索引处访问数组、bytesN 或数组切片(即 x[i] 其中 i >= x.length 或 i < 0)。
- 0x41:如果您分配了太多内存或创建了一个过大的数组。
- 0x51:如果您调用内部函数类型零初始化的变量。
此代码列表将来可能会扩展。
实际的检查算术
默认情况下,所有算术运算都会执行溢出和下溢检查(Solidity 已经有了除以零检查)。如果发生下溢或溢出,将抛出 Panic(0x11) 错误,并且调用将回滚。
例如,以下代码将触发此类错误
contract C { function f() public pure { uint x = 0; x--; } }
这些检查基于变量的实际类型,因此即使结果适合 256 位的 EVM 字,如果您的类型是 uint8 并且结果大于 255,它将触发 Panic。
我们没有为从更大类型到更小类型的显式类型转换引入检查,因为我们认为这种检查可能出乎意料。我们很乐意收到您对此事的意见!
以下运算现在有了之前没有的检查
- 加法 (+),减法 (-),乘法 (*)
- 自增和自减 (++ / --)
- 一元取反 (-)
- 指数运算 (**)
- 除法(见下文)(/)
有一些您可能没有预料到的边缘情况。例如,一元取反仅适用于有符号类型。问题在于,在二进制补码表示中,每个位宽的负数比正数多一个。这意味着以下代码将触发 Panic
function f() pure { int8 x = -128; -x; }
原因是 128 不适合 8 位有符号整数。
出于同样的原因,有符号除法会导致断言
function f() pure { int8 x = -128; x/(-1); }
任何想要为指数运算编写 SafeMath 函数的人可能都注意到这样做相当昂贵,因为 EVM 没有提供溢出信号。本质上,您必须自己实现 exp 例程,而不使用 exp 操作码来处理一般情况。
我们希望我们找到了一个相当有效的实现,并且也感谢您对此的反馈!
对于许多特殊情况,我们实际上使用 exp 操作码来实现它,而不是我们自己的实现。更具体地说,使用字面量数字作为底数的指数运算将直接使用 exp 操作码。对于值较小的变量底数也有一些专门的代码路径:所有不超过 2 的底数、3 到 10 之间的底数和 11 到 306 之间的底数都会得到特殊处理,并且应该导致一个便宜的 exp 操作码。
unchecked 块
由于检查算术使用更多的 gas(如上一节所述),并且因为在某些情况下您确实想要包装行为(例如在实现加密例程时),我们提供了一种方法来禁用检查算术并重新启用之前的 “包装” 或 “模” 算术
任何形式为 unchecked { ... } 的块都使用包装算术。您可以在另一个块中将此特殊块用作常规语句
contract C { function f() public pure returns (uint) { uint x = 0; unchecked { x--; } return x; } }
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 块包装在另一个块中才能使其工作。
如果您发现这很不方便,但如果您也认为当前的行为很好,请告诉我们!
完整变更日志(可能会有更多补充)
重大变更
- 汇编器:人工 ASSIGNIMMUTABLE 操作码和 Yul 的 “带对象访问的 EVM” 方言中的对应内置函数将要修改的代码的基偏移量作为附加参数。
- 代码生成器:默认情况下所有算术都经过检查。可以使用 unchecked { ... } 来禁用这些检查。
- 代码生成器:使用具有错误签名 Panic(uint256) 和错误代码的 revert,而不是在断言失败时使用无效操作码。
- 常规:删除全局函数 log0、log1、log2、log3 和 log4。
- 解析器:指数运算为右结合。 a**b**c 解析为 a**(b**c)。
- 类型系统: 禁止从负数字面量和大于 type(uint160).max 的字面量显式转换为 address 类型。
- 类型系统: 一元否定运算符只能用于有符号整数,不能用于无符号整数。
语言特性
- 现在可以使用成员表示法调用超级构造函数,例如 M.C(123)。
AST 变化
- 新节点 IdentifierPath 在许多地方替换了 UserDefinedTypeName。
- 新节点: unchecked 块 - 用于 unchecked { ... }。
我们希望您发现此预览版本对您有所帮助,并期待听到您对这些重大更改实施的意见。如果您有兴趣与我们讨论语言设计,请务必加入 solidity-users 邮件列表!