1. 区块链基础
  2. (一)初步理解智能合约
  3. (二)代码结构和合约特性
  4. (三)控制结构
  5. (四)类型
  6. (五)字面量和内置单位、函数
  7. (六)应用二进制接口
  8. (七)特殊函数
  9. (八)合约的高级特性(完)

Solidity 的值传递和引用传递有自己的规则,通过不同的存储域决定,后面详述。

默认值:Solidity 中不存在undefinednull,每种变量都有自己的默认值,一般是“零状态”。

运算符优先级

布尔类型

bool:常量值为truefalse

整型

int / uint :分别表示有符号和无符号的不同位数的整型变量。 支持关键字 uint8uint256 (无符号,从 8 位到 256 位)以及 int8int256,以 8 位为步长递增。 uintint 分别是 uint256int256 的别名。

可以用type(x).min type(x).max来获取这个类型地最小值和最大值。

位运算

在二进制地补码上操作,特别的 ~int256(0)== int256(-1)

移位:

左移则会截断最高位;右移操作数必须是无符号地整型,否则会编译错误。

  • x<<y相当于x*2**y,(其实这里体现了**的优先级比较高)
  • 如果x>0:x>>y相当于x/2**y
  • 如果x<0:x>>y相当于(x+1)/2**y - 1(如果不是整数,则向下取整)(注意:0.5.0 之前是向上取整)

加减乘除

0.8.0 之后加入了溢出检查,值超过上限或者下限则会回滚,我们可以使用unchecked{}来取消检查。在此之前需要使用 OpenZepplin SafeMath库。

注意:unchecked{} 不能替代码块的花括号,而且不支持嵌套,只对花括号内的语句有效,且对其中调用的函数无效,并且花括号内不能出现 _

除 0 或者模 0 会报错。type(int).min / (-1)是唯一的整除向上溢出的情况。

注意移位操作符造成的溢出并不会报错,需要额外注意溢出问题。

幂运算只适用于无符号的整型,有时为了减少 gas 消耗,编译器会建议用x*x*x来代替x**3

定义0**0=1

定长浮点型

由于 EVM 只支持整数运算并且需要严格控制计算资源,因此浮点数的计算的实现有一定的挑战,采用了严格限制整数位数和小数位数的方式。

fixed / ufixed:表示各种大小的有符号和无符号的定长浮点型。 在关键字 ufixedMxNfixedMxN 中,M 表示该类型占用的位数,N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。 ufixedfixed 分别是 ufixed128x19fixed128x19 的别名。

注意:solidity 还没有完全的支持定长浮点型,只能声明,但是不可以给他赋值,也不能用它给其他变量赋值,只可以下面那样。用的很少。

1
fixed8x4 a;

地址类型

这是比较特殊的类似,其他语言没有,实际上是储存字节。

地址类型有两种,

  • address:保存一个 20 字节的值(以太坊地址的大小),不支持作为转账地址
  • address payable可参与转账的地址,与 address 相同,不过有成员函数 transfersend

注意:addressaddress payable 的区别是在 0.5.0 版本引入的***

地址成员:

地址类型有默认的成员,方便查看它的属性。

  • <address>.balance 返回 uint256

    以 Wei 为单位的余额。

  • <address>.code 返回 bytes memory

    地址上的字节码(可以为空)

  • <address>.codehash (bytes32)

    地址上的字节码哈希

  • <address payable>.transfer(uint256 amount)

    向该地址发送数量为 amount 的 Wei,失败时抛出异常,并且会回滚。使用固定(不可调节)的 2300 gas 的矿工费。

  • <address payable>.send(uint256 amount) returns (bool)

    向该地址发送数量为 amount 的 Wei,失败时返回 false,发送 2300 gas 的矿工费用,不可调节。

    注意send安全等级比较低,他失败时(比如因为堆栈在 1024 或者 gas 不足)不会发生异常,因此往往要检查它的返回值,或者直接用transfer

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
// compiler version must be greater than or equal to 0.8.3 and less than 0.9.0
pragma solidity ^0.8.3;
contract HelloWorld {
string public greet = "Hello World!";
address public myAddress=address(this);
uint public myBalance = myAddress.balance;
bytes public myCode = myAddress.code;
bytes32 public myCodehash = myAddress.codehash;
function getstr() public view returns (string memory){
return greet;
}
}

合约类型

每一个合约都有自己的类型,也可以用合约名定义其他变量,相当于创建了一个接口。

合约可以通过address(x)转换成address类型;只有可支付的合约(具有 receive 函数或者 payable fallback 函数),才可以使用payable(address(x))转换成address payable类型(0.5.0 版本之后才区分address和payable address

下面是示例用法:

新建两个文件放在同个文件夹下:

image-20220119192540950
1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;
contract HelloWorld {
string public greet = "Hello World!";
function getstr() public view returns (string memory){
return greet;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SPDX-License-Identifier: GPL-3.0
import "./Hello.sol";
pragma solidity >=0.5.0 <0.9.0;
contract CallHello {
HelloWorld public hello;
constructor(address _addr){
hello = HelloWorld(_addr);
}
function f()public view returns(string memory){
return hello.getstr();
}
function g()public view returns(address){
return address(hello);
}
}

枚举类型

枚举类型至少需要一个成员,且不能多于 256 个成员。整数类型和枚举类型只能显式转化,不能隐式转化。整数转枚举时需要在枚举值的范围内,否则会引发Panic error

可以使用 type(NameOfEnum).mintype(NameOfEnum).max 获取这个枚举类型的最小值和最大值

1
2
3
4
5
  enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
ActionChoices choice;
function setGoStraight() public {
choice = ActionChoices.GoStraight;
}

函数类型

函数可以作为类型,可以被赋值,而且也可以作为其他函数的参数或者返回值,这一点和 Go 语言是一致的。

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.8.3;
contract A{

function foo() external pure returns(uint){
uint a =5;
return a;
}

function () external returns(uint) f=this.foo;//注意,访问函数类型,一定要从 this访问
// f=this.foo;注意无法这样赋值,只能初始化时赋值

函数类型实际上包括一个 20 个字节的地址和 4 个字节的函数选择器,等价于 byte24 类型

函数类型的调用限制

有两种:

  • 内部(internal) 函数类型,只能在当前合约内被调用(包括内部库函数和继承的函数),不能在合约的上下文外执行。调用内部函数时通过跳转到函数的标签片段实现。
  • 外部(external) 函数类型,由一个地址和函数签名组成,在调用时会被视作function类型,函数的地址后面后紧跟函数标识符一起编码成bytes24类型。

下面是函数的类型表示:

1
function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]

函数类型默认是内部函数,但是在合约内定义的函数可见性必须明确声明在合约内定义函数的位置时任意的,可以调用后面才定义的函数。

函数类型的成员

public(或 external)函数都有下面的成员:

  • .address 返回函数的合约地址。
  • .selector 返回 ABI 函数选择器

注意在过去还有两个成员:.gas(uint).value(uint) 在 0.6.2 中弃用了,在 0.7.0 中移除了。 用 {gas: ...}{value: ...} 代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.8.3;
contract A{

function foo() public pure returns(uint){
uint a =5;
return a;
}

function getAddr() public view returns(address){

return this.foo.address;
}

function getSekector() public pure returns(bytes4){
return this.foo.selector;
}
}

内部函数的例子:(这里采用了库函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
library ArrayUtils {
// 内部函数可以在内部库函数中使用,
// 因为它们会成为同一代码上下文的一部分
function map(uint[] memory self, function (uint) pure returns (uint) f)
internal
pure
returns (uint[] memory r)
{
r = new uint[](self.length);
for (uint i = 0; i < self.length; i++) {
r[i] = f(self[i]);
}
}
function reduce(
uint[] memory self,
function (uint, uint) pure returns (uint) f
)
internal
pure
returns (uint r)
{
r = self[0];
for (uint i = 1; i < self.length; i++) {
r = f(r, self[i]);
}
}
function range(uint length) internal pure returns (uint[] memory r) {
r = new uint[](length);
for (uint i = 0; i < r.length; i++) {
r[i] = i;
}
}
}

contract Pyramid {
using ArrayUtils for *;
function pyramid(uint l) public pure returns (uint) {
return ArrayUtils.range(l).map(square).reduce(sum);//前一个的返回值作为后一个的参数
}
function square(uint x) internal pure returns (uint) {
return x * x;
}
function sum(uint x, uint y) internal pure returns (uint) {
return x + y;
}
}

使用外部函数的例子:(对于不习惯将函数当作类型的读者,可能会比较陌生)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pragma solidity >=0.4.22  <0.9.0;

contract Oracle {
struct Request {
bytes data;
function(uint) external callback;
}
Request[] private requests;
event NewRequest(uint);
function query(bytes memory data, function(uint) external callback) public {
requests.push(Request(data, callback));
emit NewRequest(requests.length - 1);
}
function reply(uint requestID, uint response) public {
// 这里检查回复来自可信来源
requests[requestID].callback(response);
}
}

contract OracleUser {
Oracle constant private ORACLE_CONST = Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // known contract
uint private exchangeRate;
function buySomething() public {
ORACLE_CONST.query("USD", this.oracleResponse);
}
function oracleResponse(uint response) public {
require(
msg.sender == address(ORACLE_CONST),
"Only oracle can call this."
);
exchangeRate = response;
}
}

引用类型

引用类型可以通过不同变量名来修改指向的同一个值。目前的引用类型包括:结构体、数组和映射。

在使用引用类型时,需要指明这个类型存储在哪个数据域(data area)

  • memory:存储在内存里,只在函数内部使用,函数内变量不做特殊说明为memory类型
  • storage:相当于全局变量。函数外合约内的都是storage类型
  • calldata:保存函数的参数的特殊储存位置,只读,大多数时候和memory相似。

如果可以的话,尽可能使用calldata 临时存储传入函数的参数,因为它既不会复制,也不能修改,而且还可以作为函数的返回值。

数据的赋值

更改位置或者类型转化是拷贝;同一位置赋值一般是引用

  • storagememory之间的赋值或者用calldata对它们赋值,都是产生独立的拷贝,不修改原来的值。
  • memory之间的赋值,是引用。
  • storage给合约的全局变量赋值总是引用。
  • 其他向storage 赋值是拷贝。
  • 结构体里面的赋值是一个拷贝。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity >=0.5.0 <0.9.0;

contract C {
uint[] x; //函数外变量都默认 storage

// 函数内变量都是 memory.
function f(uint[] memory memoryArray) public {
x = memoryArray; // memory 给函数外的storage变量赋值,拷贝
uint[] storage y = x; // storage 之间 指针传递,节省内存
y.pop(); // 同时修改X
delete x; // 重置X,同时修改Y
g(x); // 函数传参时,也遵守规则,这里是传引用
h(x); //这里传复制
}

function g(uint[] storage) internal pure {}
function h(uint[] memory) public pure {}
}

数组

  • 创建多维数组时,下标的用法有些不一样,a[2][4]表示 4 个子数列,每个子数列里 2 个元素,所以a.length等于 4。但是访问数组时下标的顺序和大多数语言相同。

  • a[3],其中a也可以是数组,即 a[3] 是多维数组。

  • 多维数组的声明不要求写明长度,初始化如下 uint[][] a=[[1,2,3],[4,5,6]];,当然也可以 uint[5][7] a=[[1,2,3],[4,5,6]];,不够的位置用 0 来补上。

  • 动态数组也支持切片访问,x[start:end] 其中的startend 会隐式的转化成uint256类型,start 默认是 0,end 默认到最后,因此可以省略其中一个。切片不能够使用数组的操作成员,但是会隐式地转换成新的数组,支持进一步地按索引访问。目前,只有 calldata 的数组才支持切片。

  • 数组可以是任何类型,包括映射和结构体。但是数组中的映射只能是 storage 类型

  • bytes.concat函数可以把bytes 或者 bytes1 ... bytes32 拼接起来,返回一个bytes memory的数组。

  • .push在数列末尾添加元素,返回值是对这个元素的引用。

  • 使用 new 创建的 memory 类型的数组,内存一旦分配就是固定的,因此,不能够使用.push改变数组的大小。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    pragma solidity >=0.4.16 <0.9.0;

    contract TX {
    function f(uint len) public pure {
    uint[] memory a = new uint[](7);
    bytes memory b = new bytes(len);

    assert(a.length == 7);
    assert(b.length == len);

    a[6] = 8;
    }
    }

定长字节数组

bytes1bytes2bytes3, …, bytes32 是存放 1,2,3,直到 32 个字节的字节序列。它们看成是数组。比较特别的是,它们也可以比较大小,移位,但是不能够进行四则运算。

对于多个字节序列,可以使用bytes32[k] 之类的数组存储,但是这样使用很浪费空间,往往还是当成整体来使用,太长时就用下面将介绍的 bytes 类型.

  • byte作为bytes1的别名(在 0.8.0 之前)

  • 可以使用.length获取字节数,即字节数组长度。

变长字节数组

bytesstring,当然还有一般的数组类型如uint[]

Solidity 没有字符串操作函数但是可以使用第三方的字符串库,不过可以使用 keccak256-hash 来比较两个字符串keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)),或者使用bytes.concat(bytes(s1), bytes(s2))来连接两个字符串。

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;
contract Hello {
string public greet = "Hello, ";
function getstr(string calldata a) public view returns (string memory){
return string(bytes.concat(bytes(greet),bytes(a)));
}
}

bytesstring是特殊的数组,一方面元素的内存是紧密连续存放的,不是按照 32 个字节一单元的方式存放。其中bytes可以通过下标访问(bytes(Name)[index]或者Name[index]),返回的是底层的bytes类型的 UTF-8 码; string不能够通过下标访问。我们一般是用固定的bytes类型(如bytes1,bytes2 ,…, bytes32 ),因为 byte[] 类型的可变长数组每个元素是占 32 个字节,一个单元用不完会自动填充 0,消耗更多的 gas。

数组的赋值和字面常量

数组字面常量是在方括号中( [...] ) 包含一个或多个逗号分隔的表达式。 例如 [1, a, f(3)]

它是静态(固定大小)的 memory 类型的数组,长度由元素个数确定,数组的基本类型这样确定:

通过字面量创建的数组以列表中第一个元素的类型为准,其他的元素会隐式转化,但是这种转换需要合法。

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
function f() public pure {
g([uint(1), 2, 3]);
}
function g(uint[3] memory) public pure {
// ...
}
}

上面就是uint类型的数组字面常量。[1,-1]就是不合法的,因为正整数默认是uint8类型,而第二个元素是-1,是int8类型,数组字面常量的元素的类型就不一致了。[int8(1),-1]就是合法的。

更进一步,在 2 多维数组中,每个子列表的第一个元素都要是同样的类型:

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
function f() public pure returns (uint24[2][4] memory) {
uint24[2][4] memory x = [[uint24(0x1), 1], [0xffffff, 2], [uint24(0xff), 3], [uint24(0xffff), 4]];
// The following does not work, because some of the inner arrays are not of the right type.
// uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];
return x;
}
}

通过数组的字面常量创建数组,不支持动态分配内存,必须预设数组大小uint[] memory x = [uint(1), 3, 4];报错,必须写成uint[3] memory x = [uint(1), 3, 4];。这个考虑移除这个特性,但是会造成 ABI 中数组传参的一些麻烦。

如果是先创建 memory 的数组,再传参,也不能通过数组的字面常量赋值,必须单独给元素赋值

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
function f() public pure {
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;
}
}

数组的成员

  • .length: 返回当前数组的长度。
  • .push(): 除了string类型,其他的动态 storage 数组和bytes都可以使用这个函数在数组的末尾添加一个元素,这个元素默认是 0,返回对这个元素的引用x.push() = b,修改b即可实现对数组元素的修改。
  • .push(x): 将 x 添加到数组末尾,没有返回值。
  • .pop():除了string类型,其他的动态数组和bytes都可以使用这个函数删除数组的最后一个元素,相当于隐式地delete这个元素。(注意 delete 的效果,并不是删除)

可以看出,push增加一个元素的 gas 是固定的,因为储存单元的大小是确定的,但是使用pop()等同执行delete操作,擦除大量的空间可能会消耗很多 gas。

注意:如果需要在外部(external)函数中使用多维数组,这需要启用 ABI coder v2 (在合约最开头加上 pragma experimental ABIEncoderV2;,这是为了方便 ABI 编码)。 公有(public)函数中默认支持的使用多维数组。

注意:在 Byzantium(在 2017-10-16 日 4370000 区块上进行硬分叉升级)之前的 EVM 版本中,无法访问从函数调用返回动态数组。 如果要调用返回动态数组的函数,请确保 EVM 在拜占庭模式或者之后的模式上运行。

结构体

结构体的辖域

  • 定义在合约之外的结构体类型,可以被所有合约引用。

  • 合约内定义的结构体,只能在合约内或者是继承后的合约内可见。

  • 结构体的使用和 C 语言类似,但是注意,结构体不能使用自身。

注意:在 Solidity 0.70 以前memory结构体只有storage的成员。

结构体赋值办法:

structName(para1, para2, para3, para4) 或者 structName(paraName1:para1, paraName2:para2, paraName3:para3)

映射

映射类型在声明时的形式为 mapping(_KeyType => _ValueType)。声明映射类型的变量的形式为 mapping(_KeyType => _ValueType) _VariableName.

其中_KeyType可以是任何内置的类型,包括bytesstring以及合约类型和枚举类型,但是不能是自定义的复杂类型,映射、结构体以及数组。_ValueType可以是任何类型。但是,映射实际上是哈希表,key存储的是keccak256的哈希值而不是真实的key。因此,底层存储方式上并不是键值对的集合。

映射只能被声明为 storage 类型,不可以作为public函数的参数或返回值。如果结构体或者数组含有映射类型,也需要满足这个规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract MappingExample {
mapping(address => uint) public balances;

function update(uint newBalance) public {
balances[msg.sender] = newBalance;
}
}

contract MappingUser {
function f() public returns (uint) {
MappingExample m = new MappingExample();
m.update(100);
return m.balances(address(this));
}
}

可迭代的映射

我们使用嵌套映射和结构体,来实现复杂的数据结构,比如链表。以下例子有些难懂,这是通过位置(索引)和关键词构成的链式结构。理解思想即可,需要用到再深入学习。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

//在合约外定义的变量,是全局变量。

struct IndexValue { uint keyIndex; uint value; } //关键词对应的索引和对应的值
struct KeyFlag { uint key; bool deleted; } //标记关键词是否删除

//这类似于链表。data 用于从当前位置传递到下一个位置,每一个位置都有关键词的索引和值,构成链式结构。
//而KeyFlag 用于记录每个节点(关键词+值)是否删除
//size 标记链表长度
struct itmap {
mapping(uint => IndexValue) data;
KeyFlag[] keys;
uint size;
}

//这是库,里面很多函数可用
library IterableMapping {
//插入
function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
uint keyIndex = self.data[key].keyIndex;
self.data[key].value = value;
if (keyIndex > 0)
return true;//已经存在
else {
keyIndex = self.keys.length;

self.keys.push();
self.data[key].keyIndex = keyIndex + 1;
self.keys[keyIndex].key = key;
self.size++;
return false;
}
}

//删除
function remove(itmap storage self, uint key) internal returns (bool success) {
uint keyIndex = self.data[key].keyIndex;
if (keyIndex == 0)
return false;
delete self.data[key];
self.keys[keyIndex - 1].deleted = true;
self.size --;
}

//是否包含某个元素
function contains(itmap storage self, uint key) internal view returns (bool) {
return self.data[key].keyIndex > 0;
}

function iterate_start(itmap storage self) internal view returns (uint keyIndex) {
return iterate_next(self, type(uint).max);
}

function iterate_valid(itmap storage self, uint keyIndex) internal view returns (bool) {
return keyIndex < self.keys.length;
}

function iterate_next(itmap storage self, uint keyIndex) internal view returns (uint r_keyIndex) {
keyIndex++;
while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
keyIndex++;
return keyIndex;
}

function iterate_get(itmap storage self, uint keyIndex) internal view returns (uint key, uint value) {
key = self.keys[keyIndex].key;
value = self.data[key].value;
}
}

// 如何使用
contract User {
// Just a struct holding our data.
itmap data;
// Apply library functions to the data type.
using IterableMapping for itmap;

// Insert something
function insert(uint k, uint v) public returns (uint size) {
// This calls IterableMapping.insert(data, k, v)
data.insert(k, v);
// We can still access members of the struct,
// but we should take care not to mess with them.
return data.size;
}

// Computes the sum of all stored data.
function sum() public view returns (uint s) {
for (
uint i = data.iterate_start();
data.iterate_valid(i);
i = data.iterate_next(i)
) {
(, uint value) = data.iterate_get(i);
s += value;
}
}
}

类型转换

自定义类型

注意 type UFixed256x18 is uint256 的定义方式

UFixed256x18.unwrap(a) 从自定义类型,解封装成内置类型

UFixed256x18.wrap(a * multiplier) 从内置类型封装成自定义类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;

// Represent a 18 decimal, 256 bit wide fixed point type using a user defined value type.
type UFixed256x18 is uint256;

/// A minimal library to do fixed point operations on UFixed256x18.
library FixedMath {
uint constant multiplier = 10**18;

/// Adds two UFixed256x18 numbers. Reverts on overflow, relying on checked
/// arithmetic on uint256.
function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b));
}
/// Multiplies UFixed256x18 and uint256. Reverts on overflow, relying on checked
/// arithmetic on uint256.
function mul(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b);
}
/// Take the floor of a UFixed256x18 number.
/// @return the largest integer that does not exceed `a`.
function floor(UFixed256x18 a) internal pure returns (uint256) {
return UFixed256x18.unwrap(a) / multiplier;
}
/// Turns a uint256 into a UFixed256x18 of the same value.
/// Reverts if the integer is too large.
function toUFixed256x18(uint256 a) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(a * multiplier);
}
}

基本类型转换

隐式转换:隐式转换发生在编译时期,如果不出现信息丢失,其实都可以进行隐式转换,比如uint8可以转成uint16。隐式转换常发生在不同的操作数一起用操作符操作时发生。

显式转换:如果编译器不允许隐式转换,而你足够自信没问题,那么就去尝试显示转换,但是这很容易造成安全问题。

高版本的 Solidity 不支持常量的不符合编译器的显式转换,但是允许变量之间进行显式转换。对于intuint就是找补码,负数可以理解为下溢。如果是uint或者int同类型强制转换,就是从最低位截断(十六进制下,或者从最高位补 0。

1
2
uint32 a = 0x12345678;
uint16 b = uint16(a); // b will be 0x5678 now
1
2
3
uint16 a = 0x1234;
uint32 b = uint32(a); // b will be 0x00001234 now
assert(a == b);

对于bytes类型就是从最低位补 0 或者从最高位开始保留,这样就没有改变原来的下标。

1
2
3
4
bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b will be 0x12340000
assert(a[0] == b[0]);
assert(a[1] == b[1]);

只有具有相同字节数的整数和bytes类型才允许之间的强制转换,不同长度的需要中间过渡。注意:bytes32,表示 32 个字节,一个字节是 8 位;int256这样指的是二进制位。

1
2
3
4
5
bytes2 a = 0x1234;
uint32 b = uint16(a); // b will be 0x00001234
uint32 c = uint32(bytes4(a)); // c will be 0x12340000
uint8 d = uint8(uint16(a)); // d will be 0x34
uint8 e = uint8(bytes1(a)); // e will be 0x12

bytes数组和calldatebytes的切片转换成bytes32这样的定长字节类型,截断和填充和定长bytes一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.5;

contract C {
bytes s = "abcdefgh";
function f(bytes calldata c, bytes memory m) public view returns (bytes16, bytes3) {
require(c.length == 16, "");
bytes16 b = bytes16(m); // if length of m is greater than 16, truncation will happen
b = bytes16(s); // padded on the right, so result is "abcdefgh\0\0\0\0\0\0\0\0"
bytes3 b1 = bytes3(s); // truncated, b1 equals to "abc"
b = bytes16(c[:8]); // also padded with zeros
return (b, b1);
}
}

地址类型转换

address payable 可以完成到 address 的隐式转换,但是从 addressaddress payable 必须显式的转换, 通过 payable(<address>) 进行转换。 某些函数会严格限制采用哪一种类型。

实际上,合约类型、uint160、整数字面常量、bytes20都可以与address类型互相转换。

  • 如果有需要截断的情况,byte 类型需要转换成 uint 之后才能转换成地址类型。bytes32就会被截断,且在0.4.24之后需要做显式处理address(uint(bytes20(b))))。
  • 合约类型如果已经绑定到已部署的合约,可以显式转换成已部署合约的地址 。
  • 字面常量是 "0xabc…"的字符串,可以当作地址类型直接使用。

字面常量类型转换

  • 0.8.0 以后整型的字面产常量的强转必须在满足隐式转化的条件之上,而且整数的隐式转换非常严格,不存在截断。
  • 字节型的字面常量只支持同等大小的十六进制数转化,不能由十进制转化。但是如果字面常量是十进制的 0 或者十六进制的 0,那么就允许转换成任何的定长字节类型。
1
2
3
4
5
6
7
bytes2 a = 54321; // not allowed
bytes2 b = 0x12; // not allowed
bytes2 c = 0x123; // not allowed
bytes2 d = 0x1234; // fine
bytes2 e = 0x0012; // fine
bytes4 f = 0; // fine
bytes4 g = 0x0; // fine
  • 字符串字面常量转换成定长字节类型也需要大小相同。
1
2
3
4
5
6
bytes2 a = hex"1234"; // fine
bytes2 b = "xy"; // fine
bytes2 c = hex"12"; // not allowed
bytes2 d = hex"123"; // not allowed
bytes2 e = "x"; // not allowed
bytes2 f = "xyz"; // not allowed
  • 只有大小正确(40 位十六进制,160 个字节)的满足检验和的十六进制常量才能转换成地址类型。

函数可见性类型转化

函数的可见性类型可以发生隐式的转化,规则是:只能变得比以前更严格,不能改变原来的限制条件,只能增加更多的限制条件。

有且仅有以下三种转化:

  • pure 函数可以转换为 viewnon-payable 函数
  • view 函数可以转换为 non-payable 函数
  • payable 函数可以转换为 non-payable 函数

如果在{internal,external}的位置是public,那么函数既可以当作内部函数,也可以当作外部函数使用,如果只想当内部函数使用,就用f(函数名)调用,如果想当作外部函数调用,使用this.f(地址+函数名,合约对象.函数名)