1.6.9 Solidity V0.5.0版本与之前版本的显著区别

本节将详述0.5.0版本与之前版本较大的变化。

注意:用0.5.0编写的合约仍然可以和用老版本Solidity编写的合约及库进行交互。

1.6.9.1 单纯语义的变化

本节将罗列0.5.0版本仅仅只在语义方面发生的改变。其变化主要有下列方面:

- 有符号型数据类型的右移位运算将进行算术移位运算,而不再是单纯的四舍五入。在“Constantinople”版本中,有符号型数据类型和无符号型数据类型将有专用的操作码,但目前暂时用Solidity进行模拟。

- “do … while”循环语句中的“continue”将直接跳转到条件判断,而不再像老版本那样跳回到循环体。因此,如果此时条件判断为false,则循环中止。

- 对函数“.call()”,“.delegatecall()”和“.staticcall()”而言,当其参数为bytes时,不再对参数进行补位。

- 在“Byzantium”或者更新版本的EVM中,pure和view函数用操作码STATICCALL调用而不再用CALL调用,也就是说状态改变将在EVM中被禁止。

- 当调用abi.encode并且是外部函数调用时,ABI编码器会对calldata(msg.data和外部函数参数)的字节数组和字符串进行补位。若不想进行补位操作则要调用abi.encodePacked。

- 如果传入的calldata太短或是位数超过类型所规定的上限,则ABI解码会将calldata恢复到正常格式,但高位数据仍将被截掉。(The ABI decoder reverts in the beginning of functions and in abi.decode() if passed calldata is too short or points out of bounds. Note that dirty higher order bits are still simply ignored.)

- 自“Tangerine Whistle”版本开始,进行外部函数调用时将一并发送所有的gas余额(Forward all available gas with external function calls starting from Tangerine Whistle.)

1.6.9.2 语义和语句的变化

本节将罗列0.5.0版本在语义和语句方面都有影响的变化。

- 函数“.call()”,“.delegatecall()”,“staticcall()”,“keccak256()”,“sha256()”和“ripemd160()”在新版本中只接受一个btyes参数,并且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循环语句中,在初始化部分定义的变量在循环体内任何地方都可使用。

1.6.9.3 显式要求

本节罗列新版本在显式要求方面的一些新规定。通常对这些新规定,编译器在编译时会给出提示。

- 函数的可见性(visibility)现在必须明确定义。凡是函数以及构造函数都要加上public,凡是没有定义可见性的回退函数或接口函数都要加上external。

- 结构体,数组和映射(mapping)类型中的变量必须显式声明存储位置,对函数参数和返回值也是如此。比如老版本中的定义方式 uint[] x = m_x 在新版本中就要变成uint[] storage x = m_x,function f(uint[][] x)要变成function f(uint[][] memory x),这里memory就是存储位置,也可以用storage或calldata定义。注意,external定义的函数其参数必须用calldata定义。

- 合约类型不再包括address成员。这是为了和命名空间相区别。因此,对合约而言,在使用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。注意,在转换时要留心两者的payable回退函数是否匹配。

- 在新版本中,address类型被分为了address和address payable两种类型。只有address payable有transfer函数。address payable可以直接转换为address,但反之则不行。想把address转换为address payable可以调用uint160。比如c是个合约实例并且有payable回退函数,则address(c)将把c转换为address payable类型。如果用户使用”withdraw”模式,则多数情况下不需要做任何代码上的改动,因为transfer仅用于msg.sender,并且msg.sender是address payable类型。

- 所占字节数不同的bytesX和uintY之间的直接转换在新版本中将被禁止。因为对bytesX的补位在右侧,而对uintY的补位在左侧,两者相互转换会导致难以预料的后果。要想在两者之间进行转换,则在进行转换前要先调整好字节数。比如,想把bytes4(4个字节)转换为uint64(8个字节),则可以先把bytes4转换为bytes8,再转换为uint64。如果通过unit32进行这样的转换,则补位的方式就相反。

- 新版本中,作为安全上的考虑,将不允许在非payable函数中使用msg.value。想使用msg.value,要么把函数变为payable,要么创建一个内部函数来调用msg.value。

- 为了语义清晰,在新版本中如果标准输入作为输入源,则命令行接口现在要用“-”标识。

1.6.9.4 被否决的用法

本节罗列新版本中否决的一些旧式用法或语义。

1.6.9.4.1 命令行和JSON接口

- 命令行选项“--formal”(在老版本中用来产生Why3输出,进行验证)在新版本中已经不存在了。新版本引入了验证模块SMTChecker,其用法为:pragma experimental SMTChecker。

- 命令行选项“--julia”被重命名为“--yul”。

- 命令行选项“—clone-bin”和“—combined-json clone-bin”在新版本中不存在了。

- JSON AST的“constant”和“payable”在新版本中不存在了。它们所代表的语义在新版本中由“stateMutability”表示。

- JSON AST节点“FunctionDefinition”的“isConstructor”在新版本中被“kind”取代。“kind”取值可为“constructor”,“fallback”和“function”。

- 在未链接的二进制文件中,库地址的存储器(placeholders)在新版本中由库文件名所产生的keccak256哈希值的前36个十六进制字符表示。在旧版本中,存储器是完整的库文件名。新版本的做法减小了命名冲突的可能性,在新版本中未链接的二进制文件还包含了存储器和完整库文件名的一一映射。

1.6.9.4.2 构造函数(Constructor)

- 构造函数现在必须用关键字“constructor”显式定义。

- 调用基类的构造函数时必须带括号。

- 在同一继承关系中不允许反复声明基类构造函数的参数。

- 调用构造函数时,传入的参数数目必须匹配。如果用户仅仅只希望表达继承关系,而无需指明参数,则除了不用写参数以外,连括号都一起省略。

1.6.9.4.3 函数(Function)

- 函数callcode在新版本中被禁止了(建议改用delegatecall)。当然仍然可以在内联汇编语言中使用callcode。

- 函数suicide在新版本中被禁止(建议改用selfdestruct)。

- 函数sha3在新版本中被禁止(建议改用keccak256)。

- 函数throw在新版本中被禁止(建议改用revert,require和assert)。

1.6.9.4.4 类型转换

- 从十进制数常量(decimal literals)到bytesxx的显式和隐式类型转换在新版本中被禁止。

- 从十六进制常量(hex literals)到与其字节数不匹配的bytesxx的显式和隐式转换在新版本中被禁止。

1.6.9.4.5 常量(Literals)和后缀(Suffixes)

- 时间单位“year”在新版本中不存在了。

- 小数点后必须要跟数字。(Trailing dots that are not followed by a number are now disallowed)

- 十六进制数后不带单位。比如0x1e wei 被视为错误。

- 十六进制数的前缀在新版本中只能用“0x”而不能用“0X”(注意,后者的“X”是大写)。

1.6.9.4.6 变量

- 新版本不允许定义空的结构体(struct)。

- 关键字var在新版本中被禁用。

- 新版本中,含有不同元素的记录(tuple)之间不能互相赋值。

- 新版本中,仅仅只有非编译时常量(constant)才有赋值。

- 新版本中,对多个变量同时赋值时,值的数量和变量数量必须匹配。

- 新版本中storage变量必须要初始化。

- 新版本中记录(tuple)中的元素不可为空。

- 新版本中结构体(struct)和变量中的循环嵌套定义不可超过256个。

- 新版本中定长数组的长度不可为零。

1.6.9.4.7 语法

- 新版本中不允许使用关键字“constant”来修饰函数状态。

- 布尔表达式不再支持算术运算。

- 一元运算符“+”被禁止。

- 常量(literals)在未经显式转换为匹配的类型时不能和abi.encodePacked一起使用。

- 有返回值的函数必须要有返回值。

- 新版本中,跳转标识(jump labels),跳转语句(jump)和非函数指令被禁止使用。要实现跳转功能请使用while,switch和if。

- 没有具体实现的函数不能使用修饰符。

- 函数类型不允许带命名的返回值。

- 没有用花括号“{}”标识的代码段中,将不允许在if/while/for语句体中定义状态变量。

- 增加了新的关键字:calldata和constructor。

- 增加了新的保留关键字:alias,apply,auto,copyto,define,immutable,implements,macro,mutable,override,partial,promise,reference,sealed,sizeof,supports,typedef和unchecked。

1.6.9.5 与老版本合约的互操作

新版本合约仍然可以通过定义接口的方式和用老版本Solidity编写的合约互相操作。比如,下面这个用老版本Solidity语言编写的合约,已经部署在以太坊上了。

// 该合约无法通过新版本的编译器编译通过

pragma solidity ^0.4.25;

contract OldContract {

function someOldFunction(uint8 a) {

//...

}

function anotherOldFunction() constant returns (bool) {

//...

}

// ...

}

这个合约已经无法通过0.5.0版本的Solidity编译。但是你可以为其定义一个兼容的接口,如下所示:

pragma solidity >0.4.99 <0.6.0;

interface OldContract {

function someOldFunction(uint8 a) external;

function anotherOldFunction() external returns (bool);

}

注意,anotherOldFunction函数在原来的合约里被定义为constant,但在这里这里我们并没有把它定义为view。

在0.5.0之前,关键字constant并不是强制有效的,因此用constant修饰的函数仍然可能改变存储状态。而自0.5.0开始,staticcall将被用于调用view函数,并且当用staticcall调用一个constant函数时,函数将保持原有状态。因此当为一个老版本合约定义接口时,对老版本中用constant定义的函数,除非你确信这个函数可以用staticcall调用,否则不要用view来定义这个函数在新版中的接口。

我们为老版本合约定义了接口,现在就可以和老版本合约进行互动了。如下所示:

pragma solidity >0.4.99 <0.6.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版本之前的库,也可以通过定义库的函数来与之互动。对定义的函数不需要实现它,也不需要在链接时提供以前库的地址。如下例所示:

pragma solidity >0.4.99 <0.6.0;

library OldLibrary {

function someFunction(uint8 a) public returns(bool);

}

contract NewContract {

function f(uint8 a) public returns (bool) {

return OldLibrary.someFunction(a);

}

}

1.6.9.6 案例分析

下面我们通过展示一个新老版本合约的对比来举例说明新老版本的区别。

老版本如下:

// This will not compile

pragma solidity ^0.4.25;

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这样用,在老版本中,没问题

var z = someInteger();

x += z;

// throw 在老版本中可用

if (x > 100)

throw;

bytes b = new bytes(x);

y = -3 >> 1;

// y == -1 (wrong, should be -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 bytes) 类型的长度比bytes8 (8 bytes)小,

// 因此x的前四个字节会丢失,另外bytesX是在右侧补位,

// 这会导致意外的结果。

uint32 y = uint32(x);

myNumber += y + msg.value;

}

}

新版本如下:

pragma solidity >0.4.99 <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;

// throw 被禁用

require(x > 100);

int y = -3 >> 1;

// y == -2 (correct)

do {

x += 1;

if (x > 10) continue;

// 'Continue' 将跳转到下面的条件判断语句

} while (x < 11);

// 函数调用将返回布尔值和bytes

// Data的存储方式必须显式标识

(bool success, bytes memory data) = address(other).call("f");

if (!success)

revert();

return data;

}

using address_make_payable 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)' 不是'address payable'类型

// 所以'address(unknownContract).transfer' 不支持。

// 如果你希望向一个address发送以太币,必须先把该address用uint160转换为address payable

// 注意:我们不建议这样的用法。我们建议能用显式类型address payable的地方就尽量用它。

// 为了避免混淆,我们建议使用库函数来做这样的类型转换。

address payable addr = unknownContract.make_payable();

require(addr.send(1 ether));

// uint32 (4 bytes)类型的字节数比bytes8 (8 bytes)要少,

// 因此这种转换是不允许的。

// 我们应该先把它转换为一个通用的类型:

bytes4 x4 = bytes4(x); // 在右侧补位

uint32 y = uint32(x4); // 此种类型转换能保持一致性

// 'msg.value' 不能用于'non-payable' 函数,

// 必须将函数变为payable

myNumber += y + msg.value;

}

}

// 我们需要定义一个库来专门处理把``address``转换为

// ``address payable``的工作。

library address_make_payable {

function make_payable(address x) internal pure returns (address payable) {

return address(uint160(x));

}

}