Solidity v0.5.0 重大变更
本节重点介绍了 Solidity 版本 0.5.0 中引入的主要重大变更,以及这些变更背后的原因和如何变更日志受影响的代码。 完整列表请查看 变更日志。
备注
使用 Solidity v0.5.0 编译的合约仍然可以与使用旧版本编译的合约甚至库进行交互, 而无需重新编译或重新部署它们。 只需更改接口以包含数据位置和可见性及可变性说明符即可。 请参见下面的 与旧合约的互操作性 部分。
语义变更
本节列出了仅涉及语义的变更,因此可能会隐藏现有代码中的新行为和不同的行为。
有符号右移现在使用正确的算术右移,即向负无穷舍入,而不是向零舍入。有符号和无符号移位将在君士坦丁堡中有专用的操作码,目前由 Solidity 模拟。
do...while
循环中的continue
语句现在跳转到条件,这是这种情况下的常见行为。它以前是跳转到循环体。因此,如果条件为假,循环将终止。函数
.call()
,.delegatecall()
和.staticcall()
在给定单个bytes
参数时不再进行填充。纯函数和视图函数现在在 EVM 版本为拜占庭或更高时使用操作码
STATICCALL
调用。这禁止在 EVM 级别进行状态更改。ABI 编码器现在在外部函数调用和
abi.encode
中正确填充来自 calldata (msg.data
和外部函数参数) 的字节数组和字符串。对于未填充的编码,请使用abi.encodePacked
。如果传递的 calldata 太短或超出边界,ABI 解码器将在函数开始和
abi.decode()
中回退。请注意,脏的高位仍然会被简单忽略。从 Tangerine Whistle 开始,所有可用的 gas 都会在外部函数调用中转发。
语义和语法变更
本节重点介绍影响语法和语义的变更。
函数
.call()
,.delegatecall()
,staticcall()
,``keccak256()``,sha256()
和ripemd160()
现在只接受单个bytes
参数。 此外,参数不再填充。此更改旨在更明确和清晰地说明参数是如何连接的。 将每个.call()
(及其家族)更改为.call("")
,将每个.call(signature, a, b, c)
更改为使用.call(abi.encodeWithSignature(signature, a, b, c))
(最后一个仅适用于值类型)。 将每个keccak256(a, b, c)
更改为keccak256(abi.encodePacked(a, b, c))
。 尽管这不是重大变更,但建议开发者将x.call(bytes4(keccak256("f(uint256)")), a, b)
更改为x.call(abi.encodeWithSignature("f(uint256)", a, b))
。函数
.call()
,.delegatecall()
和.staticcall()
现在返回(bool, bytes memory)
以提供对返回数据的访问。 将bool success = otherContract.call("f")
更改为(bool success, bytes memory data) = otherContract.call("f")
。Solidity 现在实现了 C99 风格的作用域规则,对于函数局部变量,即变量只能在声明后使用,并且只能在同一作用域或嵌套作用域中使用。 在
for
循环的初始化块中声明的变量在循环内部的任何位置都是有效的。
明确性要求
本节列出了代码现在需要更明确的变更。对于大多数主题,编译器将提供建议。
显式函数可见性现在是强制性的。为每个函数和构造函数添加
public
,并为每个未指定可见性的回退或接口函数添加external
。所有结构、数组或映射类型变量的显式数据位置现在是强制性的。这也适用于函数参数和返回变量。 例如,将
uint[] x = z
更改为uint[] storage x = z
,将function f(uint[][] x)
更改为function f(uint[][] memory x)
, 其中memory
是数据位置,可能会相应地替换为storage
或calldata
。 请注意,external
函数要求参数的数据位置为calldata
。合约类型不再包含
address
成员,以便分离命名空间。因此,现在必须在使用address
成员之前显式将合约类型的值转换为地址。 示例:如果c
是一个合约,将c.transfer(...)
更改为address(c).transfer(...)
,将c.balance
更改为address(c).balance
。现在不允许在不相关的合约类型之间进行显式转换。你只能从合约类型转换为其基类或祖先类型。 如果你确定一个合约与你想要转换的合约类型兼容,尽管它不继承自它,你可以通过先转换为
address
来解决此问题。 示例:如果A
和B
是合约类型,B
不继承自A
,而b
是类型为B
的合约,你仍然可以使用A(address(b))
将b
转换为类型A
。 请注意,你仍然需要注意匹配可支付的回退函数,如下所述。address
类型被拆分为address
和address payable
,其中只有address payable
提供transfer
函数。一个address payable
可以直接转换为address
,但反向转换是不允许的。 通过uint160
转换address
为address payable
是可能的。 如果c
是一个合约,address(c)
仅在c
具有可支付的回退函数时才会产生address payable
。 如果你使用 提取模式,你很可能不需要更改代码,因为transfer
仅在msg.sender
上使用,而不是存储的地址,并且msg.sender
是一个address payable
。由于
bytesX
在右侧填充和uintY
在左侧填充可能导致意外的转换结果,因此不同大小的bytesX
和uintY
之间的转换现在不被允许。 现在必须在转换之前在类型内调整大小。例如,你可以将bytes4
(4 字节)转换为uint64
(8 字节),方法是先将bytes4
变量转换为bytes8
,然后再转换为uint64
。 通过uint32
转换时会得到相反的填充。在 v0.5.0 之前,任何bytesX
和uintY
之间的转换都会通过uint8X
进行。例如uint8(bytes3(0x291807))
将被转换为uint8(uint24(bytes3(0x291807)))
结果是0x07
)。在不可支付的函数中使用
msg.value
(或通过修改器引入它)是不允许的,作为安全功能。 将函数转换为payable
或为程序逻辑创建一个新的内部函数,该函数使用msg.value
。出于清晰原因,命令行界面现在要求在使用标准输入作为源时加上
-
。
弃用元素
本节列出了使先前功能或语法过时的更改。请注意,许多这些更改在实验模式 v0.5.0
中已经启用。
命令行和 JSON 接口
命令行选项
--formal
(用于生成进一步形式验证的 Why3 输出)已被弃用并且现在已被移除。一个新的形式验证模块 SMTChecker 通过pragma experimental SMTChecker;
启用。命令行选项
--julia
因中间语言Julia
重命名为Yul
而被重命名为--yul
。--clone-bin
和--combined-json clone-bin
命令行选项已被移除。不允许使用空前缀的重映射。
JSON AST 字段
constant
和payable
已被移除。该信息现在在stateMutability
字段中。JSON AST 字段
isConstructor
的FunctionDefinition
节点已被名为kind
的字段替代,该字段可以具有值"constructor"
,"fallback"
或"function"
。在未链接的二进制十六进制文件中,库地址占位符现在是完全限定库名称的 keccak256 哈希的前 36 个十六进制字符,周围用
$...$
包围。之前,仅使用完全限定的库名称。这减少了碰撞的可能性,特别是在使用长路径时。二进制文件现在还包含从这些占位符到完全限定名称的映射列表。
构造函数
现在必须使用
constructor
关键字定义构造函数。不再允许在没有括号的情况下调用基构造函数。
在同一继承层次结构中多次指定基构造函数参数现在是不允许的。
现在不允许以错误的参数数量调用带参数的构造函数。如果你只想指定继承关系而不提供参数,请完全不提供括号。
函数
函数
callcode
现在不被允许(支持delegatecall
)。仍然可以通过内联汇编使用它。suicide
现在不被允许(支持selfdestruct
)。sha3
现在不被允许(支持keccak256
)。throw
现在不被允许(支持revert
、require
和assert
)。
转换
从十进制字面量到
bytesXX
类型的显式和隐式转换现在不被允许。从十六进制字面量到不同大小的
bytesXX
类型的显式和隐式转换现在不被允许。
字面量和后缀
由于对闰年的复杂性和混淆,单位名称
years
现在不被允许。不再允许后面没有数字的尾随点。
现在不允许将十六进制数字与单位名称结合(例如
0x1e wei
)。十六进制数字的前缀
0X
不被允许,仅允许0x
。
变量
现在不允许声明空结构以提高清晰度。
现在不允许使用
var
关键字以支持显式性。不同组件数量的元组之间的赋值现在不被允许。
非编译时常量的常量值不被允许。
值数量不匹配的多变量声明现在不被允许。
未初始化的存储变量现在不被允许。
空元组组件现在不被允许。
在变量和结构中检测循环依赖的递归限制为 256。
长度为零的固定大小数组现在不被允许。
语法
现在不允许将
constant
用作函数状态可变性修改器。布尔表达式不能使用算术运算。
一元
+
运算符现在不被允许。字面量不能再与
abi.encodePacked
一起使用,而不先转换为显式类型。对于一个或多个返回值的函数,空返回语句现在不被允许。
“松散汇编”语法现在完全不被允许,即不再允许使用跳转标签、跳转和非功能指令。请改用新的
while
、switch
和if
构造。没有实现的函数不能再使用修改器。
带有命名返回值的函数类型现在不被允许。
在 if/while/for 体内的单语句变量声明(不是块)现在不被允许。
新关键字:
calldata
和constructor
。新保留关键字:
alias
、apply
、auto
、copyof
、define
、immutable
、implements
、macro
、mutable
、override
、partial
、promise
、reference
、sealed
、sizeof
、supports
、typedef
和unchecked
。
与旧合约的互操作性
仍然可以通过为它们定义接口与编写的 Solidity 版本低于 v0.5.0 的合约进行接口交互(或反之亦然)。假设你已经部署了以下 0.5.0 之前的版本的合约:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.25;
// 这将在编译器版本 0.4.25 之前报告警告
// 这在 0.5.0 之后将无法编译
contract OldContract {
function someOldFunction(uint8 a) {
//...
}
function anotherOldFunction() constant returns (bool) {
//...
}
// ...
}
这在 Solidity v0.5.0 中将不再编译。但是,你可以为其定义一个兼容的接口:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
interface OldContract {
function someOldFunction(uint8 a) external;
function anotherOldFunction() external returns (bool);
}
请注意,我们没有将 anotherOldFunction
声明为 view
,尽管它在原始合约中被声明为 constant
。
这是因为从 Solidity v0.5.0 开始,使用 staticcall
来调用 view` 函数。
在 v0.5.0 之前,``constant
关键字并未强制执行,因此使用 staticcall
调用声明为 constant
的函数仍可能回退,因为 constant
函数仍可能尝试修改存储。
因此,在为旧合约定义接口时,你应该仅在绝对确定该函数可以与 staticcall
一起使用的情况下,使用 view
替代 constant
。
给定上述定义的接口,你现在可以轻松使用已经部署的 0.5.0 版本之前的合约:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
interface OldContract {
function someOldFunction(uint8 a) external;
function anotherOldFunction() external returns (bool);
}
contract NewContract {
function doSomething(OldContract a) public returns (bool) {
a.someOldFunction(0x42);
return a.anotherOldFunction();
}
}
同样,可以通过定义库的函数而不实现,并在链接时提供 0.5.0 之前版本的库地址来使用库(请参见 使用命令行编译器 以了解如何使用命令行编译器进行链接):
// 这将在 0.6.0 之后无法编译
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.0;
library OldLibrary {
function someFunction(uint8 a) public returns(bool);
}
contract NewContract {
function f(uint8 a) public returns (bool) {
return OldLibrary.someFunction(a);
}
}
示例
以下示例展示了一个合约及其针对 Solidity v0.5.0 的变更日志版本,包含本节中列出的一些更改。
旧版本:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.25;
// 这将在 0.5.0 之后无法编译
contract OtherContract {
uint x;
function f(uint y) external {
x = y;
}
function() payable external {}
}
contract Old {
OtherContract other;
uint myNumber;
// 函数可变性未提供,不是错误。
function someInteger() internal returns (uint) { return 2; }
// 函数可见性未提供,不是错误。
// 函数可变性未提供,不是错误。
function f(uint x) returns (bytes) {
// 在这个版本中,变量是可以的。
var z = someInteger();
x += z;
// 抛出在这个版本中是可以的。
if (x > 100)
throw;
bytes memory b = new bytes(x);
y = -3 >> 1;
// y == -1(错误,应该是 -2)
do {
x += 1;
if (x > 10) continue;
// 'Continue' 会导致无限循环。
} while (x < 11);
// 调用只返回一个布尔值。
bool success = address(other).call("f");
if (!success)
revert();
else {
// 局部变量可以在使用后声明。
int y;
}
return b;
}
// 对于 'arr' 不需要显式数据位置
function g(uint[] arr, bytes8 x, OtherContract otherContract) public {
otherContract.transfer(1 ether);
// 由于 uint32(4 字节)小于 bytes8(8 字节), x 的前 4 字节将丢失。
// 这可能导致意外行为,因为 bytesX 是右填充的。
uint32 y = uint32(x);
myNumber += y + msg.value;
}
}
新版本:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.0;
// 这将在 0.6.0 之后无法编译
contract OtherContract {
uint x;
function f(uint y) external {
x = y;
}
function() payable external {}
}
contract New {
OtherContract other;
uint myNumber;
// 必须指定函数可变性。
function someInteger() internal pure returns (uint) { return 2; }
// 必须指定函数可见性。
// 必须指定函数可变性。
function f(uint x) public returns (bytes memory) {
// 现在必须显式给出类型。
uint z = someInteger();
x += z;
// 抛出现在是不允许的。
require(x <= 100);
int y = -3 >> 1;
require(y == -2);
do {
x += 1;
if (x > 10) continue;
// 'Continue' 跳转到下面的条件。
} while (x < 11);
// 调用返回 (bool, bytes)。
// 必须指定数据位置。
(bool success, bytes memory data) = address(other).call("f");
if (!success)
revert();
return data;
}
using AddressMakePayable for address;
// 'arr' 的数据位置必须指定
function g(uint[] memory /* arr */, bytes8 x, OtherContract otherContract, address unknownContract) public payable {
// 'otherContract.transfer' 未提供。
// 由于 'OtherContract' 的代码是已知的并且有回退
// 函数,address(otherContract) 的类型是 'address payable'。
address(otherContract).transfer(1 ether);
// 'unknownContract.transfer' 未提供。
// 'address(unknownContract).transfer' 未提供
// 因为 'address(unknownContract)' 不是 'address payable'。
// 如果函数接受一个接收资金的 'address',你可以通过 'uint160' 转换为 'address payable'。
// 注意:这不推荐,应该尽可能使用显式类型 'address payable'。
// 为了增加清晰度,我们建议使用库来进行转换(在本示例合约后提供)。
address payable addr = unknownContract.makePayable();
require(addr.send(1 ether));
// 由于 uint32(4 字节)小于 bytes8(8 字节),不允许转换。
// 我们需要先转换为相同的大小:
bytes4 x4 = bytes4(x); // 填充发生在右侧
uint32 y = uint32(x4); // 转换是一致的
// 'msg.value' 不能在 'non-payable' 函数中使用。
// 我们需要使函数可支付
myNumber += y + msg.value;
}
}
// 我们可以定义一个库来显式地将 ``address`` 转换为 ``address payable`` 作为解决方法。
library AddressMakePayable {
function makePayable(address x) internal pure returns (address payable) {
return address(uint160(x));
}
}