Solidity v0.8.0 重大变更
本节重点介绍了 Solidity 版本 0.8.0 中引入的主要重大变更。 完整列表请查看 变更日志。
语义的静默变化
本节列出了现有代码在没有编译器通知的情况下改变其行为的变化。
算术运算在下溢和上溢时会回退。你可以使用
unchecked { ... }
来使用之前的环绕行为。溢出检查非常常见,因此我们将其设为默认,以提高代码的可读性,即使这会略微增加 gas 成本。
ABI 编码器 v2 默认启用。
你可以选择使用旧行为,使用
pragma abicoder v1;
。 pragmapragma experimental ABIEncoderV2;
仍然有效,但已被弃用且没有效果。 如果你想要明确,请使用pragma abicoder v2;
。请注意,ABI 编码器 v2 支持比 v1 更多的类型,并对输入执行更多的有效性检查。 ABI 编码器 v2 使某些函数调用的成本更高,并且它还可能使合约调用 在包含不符合参数类型的数据时回退,而在使用 ABI 编码器 v1 时不会回退。
指数运算是右结合的,即表达式
a**b**c
被解析为a**(b**c)
。 在 0.8.0 之前,它被解析为(a**b)**c
。这是解析指数运算符的常见方式。
失败的断言和其他内部检查,如除以零或算术溢出,不使用无效操作码,而是使用回退操作码。 更具体地说,它们将使用等于对
Panic(uint256)
的函数调用的错误数据,错误代码特定于情况。这将节省错误时的 gas ,同时仍然允许静态分析工具区分 这些情况与无效输入的回退,例如失败的
require
。如果访问存储中长度编码不正确的字节数组,将导致恐慌。 合约无法进入这种情况,除非使用内联汇编修改存储字节数组的原始表示。
如果在数组长度表达式中使用常量,之前版本的 Solidity 会在评估树的所有分支中使用任意精度。 现在,如果常量变量用作中间表达式,它们的值将以与在运行时表达式中使用时相同的方式进行适当舍入。
类型
byte
已被移除。它是bytes1
的别名。
新限制
本节列出了可能导致现有合约无法再编译的变化。
与文字的显式转换相关的新限制。以下情况下的先前行为可能存在歧义:
不允许将负文字和大于
type(uint160).max
的文字显式转换为address
。仅当文字位于
type(T).min
和type(T).max
之间时,才允许在文字和整数类型T
之间进行显式转换。特别是,将uint(-1)
替换为type(uint).max
。仅当文字可以表示枚举中的值时,才允许在文字和枚举之间进行显式转换。
在文字和
address
类型之间的显式转换(例如address(literal)
)的类型为address
而不是address payable
。 可以通过使用显式转换来获得可支付地址类型,即payable(literal)
。
地址字面量 的类型为
address
而不是address payable
。它们可以通过使用显式转换转换为address payable
,例如payable(0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF)
。显式类型转换的新限制。仅当在符号、宽度或类型类别(
int
、address
、bytesNN
等)中最多有一次变化时,才允许转换。 要进行多次变化,请使用多次转换。让我们使用符号
T(S)
来表示显式转换T(x)
,其中T
和S
是类型,x
是类型S
的任意变量。 一个不允许的转换示例是uint16(int8)
,因为它同时改变了宽度(8 位到 16 位)和符号(有符号整数到无符号整数)。 为了进行转换,必须经过一个中间类型。在前面的示例中,这将是uint16(uint8(int8))
或uint16(int16(int8))
。 请注意,这两种转换方式将产生不同的结果,例如,对于-1
。 以下是一些被此规则禁止的转换示例。address(uint)
和uint(address)
:同时转换类型类别和宽度。将其替换为address(uint160(uint))
和uint(uint160(address))
。payable(uint160)
,payable(bytes20)
和payable(integer-literal)
:同时转换类型类别和状态可变性。 将其替换为payable(address(uint160))
,payable(address(bytes20))
和payable(address(integer-literal))
。 请注意,payable(0)
是有效的,并且是此规则的例外。int80(bytes10)
和bytes10(int80)
:同时转换类型类别和符号。将其替换为int80(uint80(bytes10))
和bytes10(uint80(int80))
。Contract(uint)
:同时转换类型类别和宽度。将其替换为Contract(address(uint160(uint)))
。
这些转换被禁止以避免歧义。例如,在表达式
uint16 x = uint16(int8(-1))
中,x
的值将取决于先应用符号转换还是宽度转换。函数调用选项只能给出一次,即
c.f{gas: 10000}{value: 1}()
是无效的,必须更改为c.f{gas: 10000, value: 1}()
。全局函数
log0
、log1
、log2
、log3
和log4
已被移除。这些是大多数未使用的低级函数。它们的行为可以通过内联汇编访问。
enum
定义不能包含超过 256 个成员。这将使得可以安全地假设 ABI 中的底层类型始终为
uint8
。名为
this
、super
和_
的声明被禁止,公共函数和事件除外。这个例外是为了使得能够声明在其他语言中实现的合约接口,这些语言允许这样的函数名称。移除对代码中
\b
、\f
和\v
转义序列的支持。 它们仍然可以通过十六进制转义插入,例如\x08
、\x0c
和\x0b
。全局变量
tx.origin
和msg.sender
的类型为address
而不是address payable
。 可以通过使用显式转换将它们转换为address payable
,即payable(tx.origin)
或payable(msg.sender)
。这个变化是因为编译器无法确定这些地址是否可支付,因此现在需要显式转换以使这一要求可见。
显式转换为
address
类型始终返回不可支付的address
类型。特别是,以下显式转换的类型为address
而不是address payable
: -address(u)
其中u
是类型为uint160
的变量。可以通过使用两个显式转换将u
转换为类型address payable
,即payable(address(u))
。 -address(b)
其中b
是类型为bytes20
的变量。可以通过使用两个显式转换将b
转换为类型address payable
,即payable(address(b))
。 -address(c)
其中c
是一个合约。之前,这种转换的返回类型取决于合约是否可以接收以太(要么有接收函数,要么有可支付的回退函数)。转换
payable(c)
的类型为address payable
,仅在合约c
可以接收以太时允许。一般来说,可以通过以下显式转换将c
转换为类型address payable
:payable(address(c))
。 请注意,address(this)
属于与address(c)
相同的类别,并且适用相同的规则。内联汇编中的
chainid
内置函数现在被视为view
而不是pure
。不再可以对无符号整数使用一元否定,只能对有符号整数使用。
接口更改
--combined-json
的输出已更改:JSON 字段abi
、devdoc
、userdoc
和storage-layout
现在是子对象。在 0.8.0 之前,它们被序列化为字符串。“遗留 AST” 已被移除(命令行接口上的
--ast-json
和标准 JSON 中的legacyAST
)。使用 “紧凑 AST”(--ast-compact-json
或AST
)作为替代。旧的错误报告器(
--old-reporter
)已被移除。
如何更新你的代码
如果你依赖于包装算术,请将每个操作包围在
unchecked { ... }
中。可选:如果你使用 SafeMath 或类似库,将
x.add(y)
更改为x + y
,将x.mul(y)
更改为x * y
等等。如果你希望保留旧的 ABI 编码器,请添加
pragma abicoder v1;
。可选地移除
pragma experimental ABIEncoderV2
或pragma abicoder v2
,因为它是多余的。将
byte
更改为bytes1
。如有需要,添加中间显式类型转换。
将
c.f{gas: 10000}{value: 1}()
合并为c.f{gas: 10000, value: 1}()
。将
msg.sender.transfer(x)
更改为payable(msg.sender).transfer(x)
或使用类型为address payable
的存储变量。将
x**y**z
更改为(x**y)**z
。使用内联汇编替代
log0
、…、log4
。通过从该类型的最大值中减去无符号整数并加 1 来否定无符号整数(例如
type(uint256).max - x + 1
,同时确保x
不为零)。