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 是数据位置,可能会相应地替换为 storagecalldata。 请注意,external 函数要求参数的数据位置为 calldata

  • 合约类型不再包含 address 成员,以便分离命名空间。因此,现在必须在使用 address 成员之前显式将合约类型的值转换为地址。 示例:如果 c 是一个合约,将 c.transfer(...) 更改为 address(c).transfer(...),将 c.balance 更改为 address(c).balance

  • 现在不允许在不相关的合约类型之间进行显式转换。你只能从合约类型转换为其基类或祖先类型。 如果你确定一个合约与你想要转换的合约类型兼容,尽管它不继承自它,你可以通过先转换为 address 来解决此问题。 示例:如果 AB 是合约类型,B 不继承自 A,而 b 是类型为 B 的合约,你仍然可以使用 A(address(b))b 转换为类型 A。 请注意,你仍然需要注意匹配可支付的回退函数,如下所述。

  • address 类型被拆分为 addressaddress payable,其中只有 address payable 提供 transfer 函数。一个 address payable 可以直接转换为 address,但反向转换是不允许的。 通过 uint160 转换 addressaddress payable 是可能的。 如果 c 是一个合约,address(c) 仅在 c 具有可支付的回退函数时才会产生 address payable。 如果你使用 提取模式,你很可能不需要更改代码,因为 transfer 仅在 msg.sender 上使用,而不是存储的地址,并且 msg.sender 是一个 address payable

  • 由于 bytesX 在右侧填充和 uintY 在左侧填充可能导致意外的转换结果,因此不同大小的 bytesXuintY 之间的转换现在不被允许。 现在必须在转换之前在类型内调整大小。例如,你可以将 bytes4 (4 字节)转换为 uint64 (8 字节),方法是先将 bytes4 变量转换为 bytes8,然后再转换为 uint64。 通过 uint32 转换时会得到相反的填充。在 v0.5.0 之前,任何 bytesXuintY 之间的转换都会通过 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 字段 constantpayable 已被移除。该信息现在在 stateMutability 字段中。

  • JSON AST 字段 isConstructorFunctionDefinition 节点已被名为 kind 的字段替代,该字段可以具有值 "constructor", "fallback""function"

  • 在未链接的二进制十六进制文件中,库地址占位符现在是完全限定库名称的 keccak256 哈希的前 36 个十六进制字符,周围用 $...$ 包围。之前,仅使用完全限定的库名称。这减少了碰撞的可能性,特别是在使用长路径时。二进制文件现在还包含从这些占位符到完全限定名称的映射列表。

构造函数

  • 现在必须使用 constructor 关键字定义构造函数。

  • 不再允许在没有括号的情况下调用基构造函数。

  • 在同一继承层次结构中多次指定基构造函数参数现在是不允许的。

  • 现在不允许以错误的参数数量调用带参数的构造函数。如果你只想指定继承关系而不提供参数,请完全不提供括号。

函数

  • 函数 callcode 现在不被允许(支持 delegatecall)。仍然可以通过内联汇编使用它。

  • suicide 现在不被允许(支持 selfdestruct)。

  • sha3 现在不被允许(支持 keccak256)。

  • throw 现在不被允许(支持 revertrequireassert)。

转换

  • 从十进制字面量到 bytesXX 类型的显式和隐式转换现在不被允许。

  • 从十六进制字面量到不同大小的 bytesXX 类型的显式和隐式转换现在不被允许。

字面量和后缀

  • 由于对闰年的复杂性和混淆,单位名称 years 现在不被允许。

  • 不再允许后面没有数字的尾随点。

  • 现在不允许将十六进制数字与单位名称结合(例如 0x1e wei)。

  • 十六进制数字的前缀 0X 不被允许,仅允许 0x

变量

  • 现在不允许声明空结构以提高清晰度。

  • 现在不允许使用 var 关键字以支持显式性。

  • 不同组件数量的元组之间的赋值现在不被允许。

  • 非编译时常量的常量值不被允许。

  • 值数量不匹配的多变量声明现在不被允许。

  • 未初始化的存储变量现在不被允许。

  • 空元组组件现在不被允许。

  • 在变量和结构中检测循环依赖的递归限制为 256。

  • 长度为零的固定大小数组现在不被允许。

语法

  • 现在不允许将 constant 用作函数状态可变性修改器。

  • 布尔表达式不能使用算术运算。

  • 一元 + 运算符现在不被允许。

  • 字面量不能再与 abi.encodePacked 一起使用,而不先转换为显式类型。

  • 对于一个或多个返回值的函数,空返回语句现在不被允许。

  • “松散汇编”语法现在完全不被允许,即不再允许使用跳转标签、跳转和非功能指令。请改用新的 whileswitchif 构造。

  • 没有实现的函数不能再使用修改器。

  • 带有命名返回值的函数类型现在不被允许。

  • 在 if/while/for 体内的单语句变量声明(不是块)现在不被允许。

  • 新关键字:calldataconstructor

  • 新保留关键字:aliasapplyautocopyofdefineimmutableimplementsmacromutableoverridepartialpromisereferencesealedsizeofsupportstypedefunchecked

与旧合约的互操作性

仍然可以通过为它们定义接口与编写的 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));
    }
}