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));
}
}