Solidity v0.5.0 突破性变更
本节重点介绍 Solidity 版本 0.5.0 中引入的主要突破性变更,以及变更背后的原因以及如何更新受影响的代码。有关完整列表,请查看 发布变更日志.
注意
使用 Solidity v0.5.0 编译的合约仍然可以与使用旧版本编译的合约甚至库进行交互,而无需重新编译或重新部署它们。更改接口以包含数据位置和可见性以及可变性说明符就足够了。请参阅下面的 与旧版合约的互操作性 部分。
仅语义更改
本节列出了仅限语义的更改,因此可能隐藏了现有代码中的新行为和不同行为。
有符号右移现在使用正确的算术移位,即朝负无穷大舍入,而不是朝零舍入。有符号和无符号移位将在 Constantinople 中拥有专用的操作码,并且 Solidity 暂时对其进行模拟。
continue语句在do...while循环中现在跳到条件,这是此类情况下常见的行为。它曾经跳到循环体。因此,如果条件为假,循环将终止。函数
.call()、.delegatecall()和.staticcall()当给定单个bytes参数时不再进行填充。如果 EVM 版本为 Byzantium 或更高版本,则现在使用操作码
STATICCALL而不是CALL来调用纯函数和视图函数。这禁止了 EVM 级别的状态更改。ABI 编码器现在在外部函数调用中以及在
abi.encode中使用时,会正确填充来自调用数据的字节数组和字符串(msg.data和外部函数参数)。对于未填充的编码,请使用abi.encodePacked。如果传递的调用数据太短或指向越界,则 ABI 解码器将在函数开头和
abi.decode()中恢复。请注意,脏的高位仍然被简单地忽略。从 Tangerine Whistle 开始,使用外部函数调用转发所有可用汽油。
语义和语法更改
本节重点介绍影响语法和语义的更改。
函数
.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变量转换为bytes8,然后转换为uint64,来将一个bytes4(4 字节)转换为uint64(8 字节)。通过uint32进行转换时,您会得到相反的填充。在 v0.5.0 之前,任何bytesX和uintY之间的转换都将通过uint8X进行。例如uint8(bytes3(0x291807))将被转换为uint8(uint24(bytes3(0x291807)))(结果为0x07)。出于安全考虑,在非 payable 函数中使用
msg.value(或通过修饰符引入它)是被禁止的。将函数变成payable或为使用msg.value的程序逻辑创建一个新的内部函数。为了清晰起见,命令行界面现在要求在使用标准输入作为源时使用
-。
已弃用的元素
本节列出了弃用先前功能或语法的更改。请注意,这些更改中的许多已经在实验模式 v0.5.0 中启用。
命令行和 JSON 接口
命令行选项
--formal(用于生成 Why3 输出以进行进一步的形式验证)已被弃用,现在已删除。一个新的形式验证模块,SMTChecker,通过pragma experimental SMTChecker;启用。命令行选项
--julia已重命名为--yul,因为中间语言Julia已重命名为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一起使用。现在不允许对具有一个或多个返回值的函数使用空 return 语句。
现在完全不允许使用“松散汇编”语法,即跳转标签、跳转和非功能指令不再可以使用。请改用新的
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;
// This will report a warning until version 0.4.25 of the compiler
// This will not compile after 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.5.0 库(有关如何使用命令行编译器进行链接,请参阅 使用命令行编译器)
// This will not compile after 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;
// This will not compile after 0.5.0
contract OtherContract {
uint x;
function f(uint y) external {
x = y;
}
function() payable external {}
}
contract Old {
OtherContract other;
uint myNumber;
// Function mutability not provided, not an error.
function someInteger() internal returns (uint) { return 2; }
// Function visibility not provided, not an error.
// Function mutability not provided, not an error.
function f(uint x) returns (bytes) {
// Var is fine in this version.
var z = someInteger();
x += z;
// Throw is fine in this version.
if (x > 100)
throw;
bytes memory b = new bytes(x);
y = -3 >> 1;
// y == -1 (wrong, should be -2)
do {
x += 1;
if (x > 10) continue;
// 'Continue' causes an infinite loop.
} while (x < 11);
// Call returns only a Bool.
bool success = address(other).call("f");
if (!success)
revert();
else {
// Local variables could be declared after their use.
int y;
}
return b;
}
// No need for an explicit data location for 'arr'
function g(uint[] arr, bytes8 x, OtherContract otherContract) public {
otherContract.transfer(1 ether);
// Since uint32 (4 bytes) is smaller than bytes8 (8 bytes),
// the first 4 bytes of x will be lost. This might lead to
// unexpected behavior since bytesX are right padded.
uint32 y = uint32(x);
myNumber += y + msg.value;
}
}
新版本
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.0;
// This will not compile after 0.6.0
contract OtherContract {
uint x;
function f(uint y) external {
x = y;
}
function() payable external {}
}
contract New {
OtherContract other;
uint myNumber;
// Function mutability must be specified.
function someInteger() internal pure returns (uint) { return 2; }
// Function visibility must be specified.
// Function mutability must be specified.
function f(uint x) public returns (bytes memory) {
// The type must now be explicitly given.
uint z = someInteger();
x += z;
// Throw is now disallowed.
require(x <= 100);
int y = -3 >> 1;
require(y == -2);
do {
x += 1;
if (x > 10) continue;
// 'Continue' jumps to the condition below.
} while (x < 11);
// Call returns (bool, bytes).
// Data location must be specified.
(bool success, bytes memory data) = address(other).call("f");
if (!success)
revert();
return data;
}
using AddressMakePayable for address;
// Data location for 'arr' must be specified
function g(uint[] memory /* arr */, bytes8 x, OtherContract otherContract, address unknownContract) public payable {
// 'otherContract.transfer' is not provided.
// Since the code of 'OtherContract' is known and has the fallback
// function, address(otherContract) has type 'address payable'.
address(otherContract).transfer(1 ether);
// 'unknownContract.transfer' is not provided.
// 'address(unknownContract).transfer' is not provided
// since 'address(unknownContract)' is not 'address payable'.
// If the function takes an 'address' which you want to send
// funds to, you can convert it to 'address payable' via 'uint160'.
// Note: This is not recommended and the explicit type
// 'address payable' should be used whenever possible.
// To increase clarity, we suggest the use of a library for
// the conversion (provided after the contract in this example).
address payable addr = unknownContract.makePayable();
require(addr.send(1 ether));
// Since uint32 (4 bytes) is smaller than bytes8 (8 bytes),
// the conversion is not allowed.
// We need to convert to a common size first:
bytes4 x4 = bytes4(x); // Padding happens on the right
uint32 y = uint32(x4); // Conversion is consistent
// 'msg.value' cannot be used in a 'non-payable' function.
// We need to make the function payable
myNumber += y + msg.value;
}
}
// We can define a library for explicitly converting ``address``
// to ``address payable`` as a workaround.
library AddressMakePayable {
function makePayable(address x) internal pure returns (address payable) {
return address(uint160(x));
}
}