数据类型

本文介绍Solidity中的数据类型

  • boolean
  • uint
  • int
  • address
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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Primitives {
bool public boo = true;

/*
uint 无符号整数,意思是非负整数
有不同范围类型的uint可供选择
uint8 范围从 0 到 2 ** 8 - 1
uint16 范围从 0 到 2 ** 16 - 1
...
uint256 范围从 0 到 2 ** 256 - 1
*/
uint8 public u8 = 1;
uint public u256 = 456;
uint public u = 123; // uint实际是uint256的别名,它和uint256的取值范围是一样的

/*
int类型允许使用负数
和uint一样,它也提供了int8到int256不同范围的int类型

int256 范围从 -2 ** 255 到 2 ** 255 - 1
int128 范围从 -2 ** 127 到 2 ** 127 - 1
*/
int8 public i8 = -1;
int public i256 = 456;
int public i = -123; // int实际是int256的别名,它和int256的取值范围是一样的

// int类型的最小值和最大值
int public minInt = type(int).min;
int public maxInt = type(int).max;

address public addr = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c;

/*
在 Solidity 中,数据类型 bytes 表示一个byte序列。
Solidity 提供了两种 bytes 类型:

- 固定大小的byte数组
- 动态大小的byte数组

bytes表示动态大小的byte数组
bytes1、bytes2、.......bytes32表示不同固定大小的byte数组
*/
bytes1 a = 0xb5; // [10110101]
bytes1 b = 0x56; // [01010110]

// 默认值
// 未赋值的变量都有一个默认值
bool public defaultBoo; // false
uint public defaultUint; // 0
int public defaultInt; // 0
address public defaultAddr; // 0x0000000000000000000000000000000000000000
}

变量

Solidity中有3中类型的变量

  • local 本地变量
    • 在函数内部声明
    • 不存在区块链上
  • state 状态变量
    • 在函数外声明
    • 存储在区块链上
  • global 全局变量(提供有关区块链的信息)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Variables {
// 状态变量存储在区块链上
string public text = "Hello";
uint public num = 123;

function doSomething() public {
// 局部的本地变量不会存储在区块链上
uint i = 456;

// 下面是一些全局变量
uint timestamp = block.timestamp; // 当前区块的时间戳
address sender = msg.sender; // 调用当前合约的账户地址
}
}

常量

常量是不能被修改的变量,它的值是硬编码的,使用常量可以节省gas成本。

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Constants {
// 通常约定使用全大写字母定义常量
address public constant MY_ADDRESS = 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc;
uint public constant MY_UINT = 123;
}

不可变变量

不可变变量和常量类似,但不可变变量的值可以在构造函数中进行设置,之后就不能被修改了。

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Immutable {
// 也使用全大写字母定义不可变变量,只是关键字为immutable
address public immutable MY_ADDRESS;
uint public immutable MY_UINT;

constructor(uint _myUint) {
MY_ADDRESS = msg.sender;
MY_UINT = _myUint;
}
}

读取和写入状态变量

要写入或者更新状态变量,必须要发送交易才行。读取状态变量则是免费的,不需要任何交易费用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract SimpleStorage {
// 定义一个用于存储数字的状态变量
uint public num;

// 调用set方法时需要发送交易方可写入状态变量
function set(uint _num) public {
num = _num;
}

// 调用get方法时不需要发送交易则可以读取状态变量中的值
function get() public view returns (uint) {
return num;
}
}

Ether 和 Wei 的关系

交易费用是用以太币支付的,类似于1美元等于100美分,1以太币(Ether)等于10的18次方Wei.

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract EtherUnits {
uint public oneWei = 1 wei;
// 1 wei 和 1 相等
bool public isOneWei = 1 wei == 1;

uint public oneEther = 1 ether;
// 1 ether 和 10^18 wei 相等
bool public isOneEther = 1 ether == 1e18;
}

Gas

你需要为一笔交易支付多少以太币?

交易费=你支付花费的gas * gas 价格,即:gas=gas spent * gas price,其中:

  • gas 是计算单位
  • gas spent 是交易中使用的gas总量
  • gas price 是您愿意为每个gas支付多少以太币

gas价格较高的交易具有更高的优先级被包含在下一个区块中。

未用完的gas将被退还。

Gas Limit

您可以花费的gas数量有2个上限

  • gas limit(您愿意为交易花费的的最大gas数量,由您发送交易时自己设置)
  • block gas limit(区块中允许的最大gas数量,由网络设置)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Gas {
uint public i = 0;

// 用完您发送的所有gas会导致您的交易失败
// 状态更改会被撤销
// 消耗的gas不予退还
function forever() public {
// 在这里我们运行一个死循环,直到所有的gas都用完,并且交易失败
while (true) {
i += 1;
}
}
}

if/else

Solidity中支持条件语句if、else if和else

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract IfElse {
function foo(uint x) public pure returns (uint) {
if (x < 10) {
return 0;
} else if (x < 20) {
return 1;
} else {
return 2;
}
}

function ternary(uint _x) public pure returns (uint) {
// if (_x < 10) {
// return 1;
// }
// return 2;

// if / else 的简写方式
// 这里的 "?" 操作符被称为三元运算符
return _x < 10 ? 1 : 2;
}
}

For/While

Solidity 支持 for、while 和 do while 循环。

不要编写死循环,因为这可能会达到 gas 限制,从而导致您的交易失败。

由于上述原因,一般在合约中很少使用 while 和 do while 循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Loop {
function loop() public {
// for loop
for (uint i = 0; i < 10; i++) {
if (i == 3) {
// Skip to next iteration with continue
continue;
}
if (i == 5) {
// Exit loop with break
break;
}
}

// while loop
uint j;
while (j < 10) {
j++;
}
}
}

Mapping 映射

Solidity中map类型是使用mapping(keyType => valueType)方式创建的。keyType 可以是任何内置值类型、bytes、string或任何合约。valueType 可以是任何类型,包括另一个映射或数组。

mapping类型是不可迭代的。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Mapping {
// Mapping from address to uint
mapping(address => uint) public myMap;

function get(address _addr) public view returns (uint) {
// 从mapping中取值时,总能返回一个值.
// 如果该值从未设置,它将返回默认值
return myMap[_addr];
}

function set(address _addr, uint _i) public {
// 更新此地址的值
myMap[_addr] = _i;
}

function remove(address _addr) public {
// 将此地址的值重置为默认值
delete myMap[_addr];
}
}

contract NestedMapping {
// 嵌套映射
mapping(address => mapping(uint => bool)) public nested;

function get(address _addr1, uint _i) public view returns (bool) {
// 从嵌套映射中获取值,即使没有初始化
return nested[_addr1][_i];
}

function set(
address _addr1,
uint _i,
bool _boo
) public {
nested[_addr1][_i] = _boo;
}

function remove(address _addr1, uint _i) public {
delete nested[_addr1][_i];
}
}

Array 数组

数组可以具有编译时固定大小或动态大小。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Array {
// 初始化数组的几种方法
uint[] public arr;
uint[] public arr2 = [1, 2, 3];
// 固定大小的数组,所有元素初始化为 0
uint[10] public myFixedSizeArr;

function get(uint i) public view returns (uint) {
return arr[i];
}

// Solidity 可以返回整个数组
// 但是应该避免使用此功能,长度可以无限增长的数组。
function getArr() public view returns (uint[] memory) {
return arr;
}

function push(uint i) public {
// 向数组添加元素
// 这将使数组长度增加 1.
arr.push(i);
}

function pop() public {
// 从数组中删除最后一个元素
// 这将使数组长度减1
arr.pop();
}

function getLength() public view returns (uint) {
return arr.length;
}

function remove(uint index) public {
// delete 不会改变整个数组的长度
// 只会将该索引位置的元素设置为默认值
delete arr[index];
}

function examples() external {
// 在内存中创建数组,只能创建固定大小
uint[] memory a = new uint[](5);
}
}

移除数组元素的例子

通过从右向左移动元素来删除数组元素

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract ArrayRemoveByShifting {
// [1, 2, 3] -- remove(1) --> [1, 3, 3] --> [1, 3]
// [1, 2, 3, 4, 5, 6] -- remove(2) --> [1, 2, 4, 5, 6, 6] --> [1, 2, 4, 5, 6]
// [1, 2, 3, 4, 5, 6] -- remove(0) --> [2, 3, 4, 5, 6, 6] --> [2, 3, 4, 5, 6]
// [1] -- remove(0) --> [1] --> []

uint[] public arr;

function remove(uint _index) public {
require(_index < arr.length, "index out of bound");

for (uint i = _index; i < arr.length - 1; i++) {
arr[i] = arr[i + 1];
}
arr.pop();
}

function test() external {
arr = [1, 2, 3, 4, 5];
remove(2);
// [1, 2, 4, 5]
assert(arr[0] == 1);
assert(arr[1] == 2);
assert(arr[2] == 4);
assert(arr[3] == 5);
assert(arr.length == 4);

arr = [1];
remove(0);
// []
assert(arr.length == 0);
}
}

通过将最后一个元素复制到要删除的位置来删除数组元素

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract ArrayReplaceFromEnd {
uint[] public arr;

// Deleting an element creates a gap in the array.
// One trick to keep the array compact is to
// move the last element into the place to delete.
function remove(uint index) public {
// Move the last element into the place to delete
arr[index] = arr[arr.length - 1];
// Remove the last element
arr.pop();
}

function test() public {
arr = [1, 2, 3, 4];

remove(1);
// [1, 4, 3]
assert(arr.length == 3);
assert(arr[0] == 1);
assert(arr[1] == 4);
assert(arr[2] == 3);

remove(2);
// [1, 4]
assert(arr.length == 2);
assert(arr[0] == 1);
assert(arr[1] == 4);
}
}

Enum 枚举

Solidity 支持枚举类型,它们对于建模选择和跟踪状态很有用。

枚举类型可以在合约之外声明。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Enum {
// 表示运输状态的枚举
enum Status {
Pending,
Shipped,
Accepted,
Rejected,
Canceled
}

// 默认值是定义的枚举类型中的第一项,在本例中为 "Pending"
Status public status;

// Returns uint
// Pending - 0
// Shipped - 1
// Accepted - 2
// Rejected - 3
// Canceled - 4
function get() public view returns (Status) {
return status;
}

// 通过传入 uint 类型来更新状态
function set(Status _status) public {
status = _status;
}

// 可以这样更新特定的枚举值
function cancel() public {
status = Status.Canceled;
}

// delete 将枚举值重置为第一个值,0
function reset() public {
delete status;
}
}

声明和导入枚举

声明枚举的文件

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// This is saved 'EnumDeclaration.sol'

enum Status {
Pending,
Shipped,
Accepted,
Rejected,
Canceled
}

导入上述的枚举文件

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "./EnumDeclaration.sol";

contract Enum {
Status public status;
}

Structs 结构

您可以通过创建结构来定义自己的类型。

它们对于将相关数据分组在一起很有用。

结构可以在合同之外声明并在另一个合同中导入。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Todos {
struct Todo {
string text;
bool completed;
}

// 'Todo'结构类型的数组
Todo[] public todos;

function create(string calldata _text) public {
// 3种初始化结构的方法
// - 像函数一样调用它
todos.push(Todo(_text, false));

// 键值映射
todos.push(Todo({text: _text, completed: false}));

// 初始化一个空结构然后更新它
Todo memory todo;
todo.text = _text;
// todo.completed 初始化为 false

todos.push(todo);
}

// Solidity automatically created a getter for 'todos' so
// you don't actually need this function.
function get(uint _index) public view returns (string memory text, bool completed) {
Todo storage todo = todos[_index];
return (todo.text, todo.completed);
}

// update text
function updateText(uint _index, string calldata _text) public {
Todo storage todo = todos[_index];
todo.text = _text;
}

// update completed
function toggleCompleted(uint _index) public {
Todo storage todo = todos[_index];
todo.completed = !todo.completed;
}
}

声明和导入结构

声明结构的文件

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// This is saved 'StructDeclaration.sol'

struct Todo {
string text;
bool completed;
}

导入上述结构的文件

1
2
3
4
5
6
7
8
9
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "./StructDeclaration.sol";

contract Todos {
// An array of 'Todo' structs
Todo[] public todos;
}

数据位置

在Solidity中,变量可以被声明为storagememorycalldata来显式指定数据的位置。

  • storage 变量是状态变量(存储在区块链上)
  • memory 变量在内存中,并且在调用函数时存在
  • calldata 包含函数参数的特殊数据位置
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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract DataLocations {
uint[] public arr;
mapping(uint => address) map;
struct MyStruct {
uint foo;
}
mapping(uint => MyStruct) myStructs;

function f() public {
// call _f with state variables
_f(arr, map, myStructs[1]);

// get a struct from a mapping
MyStruct storage myStruct = myStructs[1];
// 在内存中创建一个结构
MyStruct memory myMemStruct = MyStruct(0);
}

function _f(
uint[] storage _arr,
mapping(uint => address) storage _map,
MyStruct storage _myStruct
) internal {
// do something with storage variables
}

// You can return memory variables
function g(uint[] memory _arr) public returns (uint[] memory) {
// do something with memory array
}

function h(uint[] calldata _arr) external {
// do something with calldata array
}
}

Function 函数

有几种方法可以从函数返回输出。 公共函数不能接受某些数据类型作为输入或输出。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Function {
// 函数可以返回多个值。
function returnMany()
public
pure
returns (
uint,
bool,
uint
)
{
return (1, true, 2);
}

// 返回值可以命名。
function named()
public
pure
returns (
uint x,
bool b,
uint y
)
{
return (1, true, 2);
}

// 返回值可以分配给他们的名字。
// 在这种情况下,return 语句可以省略。
function assigned()
public
pure
returns (
uint x,
bool b,
uint y
)
{
x = 1;
b = true;
y = 2;
}

// 调用另一个时使用解构赋值,返回多个值的函数。
function destructuringAssignments()
public
pure
returns (
uint,
bool,
uint,
uint,
uint
)
{
(uint i, bool b, uint j) = returnMany();

// 值可以省略.
(uint x, , uint y) = (4, 5, 6);

return (i, b, j, x, y);
}

// 不能将mapping类型用于输入或输出

// 可以使用数组作为输入
function arrayInput(uint[] memory _arr) public {}

// 可以使用数组进行输出
uint[] public arr;

function arrayOutput() public view returns (uint[] memory) {
return arr;
}
}

View 和 Pure 函数

Getter 函数可以声明为 view 或 pure。

View 函数声明不会更改任何状态。

Pure 纯函数声明不会更改或读取任何状态变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract ViewAndPure {
uint public x = 1;

// 承诺不修改状态。
function addToX(uint y) public view returns (uint) {
return x + y;
}

// 承诺不修改或读取状态。
function add(uint i, uint j) public pure returns (uint) {
return i + j;
}
}

Error

错误将撤消事务期间对状态所做的所有更改。

您可以通过调用 require、revert 或 assert 来引发错误。

  • require 用于在执行之前验证输入和条件
  • revert 类似于 require ,有关详细信息,请参阅下面的代码
  • assert 用于检查不应该为假的代码。断言失败可能意味着存在错误。

使用自定义错误来节省Gas.

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Error {
function testRequire(uint _i) public pure {
// Require 应该用于验证条件,例如:
// - 输入
// - 执行前的条件
// - 从调用其他函数返回值
require(_i > 10, "Input must be greater than 10");
}

function testRevert(uint _i) public pure {
// 当要检查的条件很复杂时,revert 很有用。
// 这段代码和上面的例子完全一样
if (_i <= 10) {
revert("Input must be greater than 10");
}
}

uint public num;

function testAssert() public view {
// 断言只能用于测试内部错误, 并检查不变量。

// 这里我们断言 num 总是等于 0, 因为不可能更新 num 的值
assert(num == 0);
}

// 自定义错误
error InsufficientBalance(uint balance, uint withdrawAmount);

function testCustomError(uint _withdrawAmount) public view {
uint bal = address(this).balance;
if (bal < _withdrawAmount) {
revert InsufficientBalance({balance: bal, withdrawAmount: _withdrawAmount});
}
}
}

这是另一个例子

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Account {
uint public balance;
uint public constant MAX_UINT = 2**256 - 1;

function deposit(uint _amount) public {
uint oldBalance = balance;
uint newBalance = balance + _amount;

// balance + _amount does not overflow if balance + _amount >= balance
require(newBalance >= oldBalance, "Overflow");

balance = newBalance;

assert(balance >= oldBalance);
}

function withdraw(uint _amount) public {
uint oldBalance = balance;

// balance - _amount does not underflow if balance >= _amount
require(balance >= _amount, "Underflow");

if (balance < _amount) {
revert("Underflow");
}

balance -= _amount;

assert(balance <= oldBalance);
}
}

Modifier 函数修饰符

修饰符是可以在函数调用之前和/或之后运行的代码。

修饰符可用于:

  • 限制访问
  • 验证输入
  • 防范黑客重入攻击
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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract FunctionModifier {
// 我们将使用这些变量来演示如何使用 Modifier 修饰符。
address public owner;
uint public x = 10;
bool public locked;

constructor() {
// 将交易发送者设置为合约的所有者。
owner = msg.sender;
}

// 定义修饰符,用于检查调用者是否是合约所有者。
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
// 下划线是仅在函数修饰符内部使用的特殊字符,它告诉 Solidity 执行函数的其余代码。
_;
}

// 修饰符可以接受输入, 这个修饰符检查传入的地址不是零地址。
modifier validAddress(address _addr) {
require(_addr != address(0), "Not valid address");
_;
}

function changeOwner(address _newOwner) public onlyOwner validAddress(_newOwner) {
owner = _newOwner;
}

// 修饰符可以在函数之前和/或之后调用。 此修饰符可防止函数在仍在执行时被调用。
modifier noReentrancy() {
require(!locked, "No reentrancy");

locked = true;
_;
locked = false;
}

function decrement(uint i) public noReentrancy {
x -= i;

if (i > 1) {
decrement(i - 1);
}
}
}

Events 事件

事件允许登录到以太坊区块链。 事件的一些用例是:

  • 监听事件和更新用户界面
  • 一种廉价的存储方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Event {
// 事件声明
// 最多可以索引 3 个参数。
// 索引参数帮助您通过索引参数过滤日志
event Log(address indexed sender, string message);
event AnotherLog();

function test() public {
emit Log(msg.sender, "Hello World!");
emit Log(msg.sender, "Hello EVM!");
emit AnotherLog();
}
}

Constructor 构造函数

构造函数是在合约创建时执行的可选函数。

以下是如何将参数传递给构造函数的示例。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

// 基础合约 X
contract X {
string public name;

constructor(string memory _name) {
name = _name;
}
}

// 基础合约 Y
contract Y {
string public text;

constructor(string memory _text) {
text = _text;
}
}

// 有两种方法可以使用参数初始化父合约。

// 在继承列表中传递参数。
contract B is X("Input to X"), Y("Input to Y") {

}

contract C is X, Y {
// 在构造函数中传递参数,类似于函数修饰符。
constructor(string memory _name, string memory _text) X(_name) Y(_text) {}
}

// 无论子合约的构造函数中列出的父合约的顺序如何,始终按继承顺序调用父构造函数。

// 调用的构造函数顺序:
// 1. X
// 2. Y
// 3. D
contract D is X, Y {
constructor() X("X was called") Y("Y was called") {}
}

// 调用构造函数的顺序:
// 1. X
// 2. Y
// 3. E
contract E is X, Y {
constructor() Y("Y was called") X("X was called") {}
}

Inheritance 遗产

Solidity 支持多重继承。合约可以使用is关键字继承其他合约。

将被子合同覆盖的函数必须声明为virtual.

将要覆盖父函数的函数必须使用关键字override

继承顺序很重要。

您必须按照从“最基础”到“最衍生”的顺序列出父合约。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

/* Graph of inheritance
A
/ \
B C
/ \ /
F D,E

*/

contract A {
function foo() public pure virtual returns (string memory) {
return "A";
}
}

// 合约通过使用关键字“is”继承其他合约。
contract B is A {
// Override A.foo()
function foo() public pure virtual override returns (string memory) {
return "B";
}
}

contract C is A {
// Override A.foo()
function foo() public pure virtual override returns (string memory) {
return "C";
}
}

// 合约可以从多个父合约继承。 当调用在不同合约中多次定义的函数时,父合约会从右到左以深度优先的方式进行搜索。

contract D is B, C {
// D.foo() returns "C"
// 因为 C 是具有函数 foo() 的最正确的父合约
function foo() public pure override(B, C) returns (string memory) {
return super.foo();
}
}

contract E is C, B {
// E.foo() returns "B"
// 因为 B 是具有函数 foo() 的最正确的父合约
function foo() public pure override(C, B) returns (string memory) {
return super.foo();
}
}

// 继承必须从“最类似基础”到“最衍生”排序。
// 交换 A 和 B 的顺序会抛出编译错误。
contract F is A, B {
function foo() public pure override(A, B) returns (string memory) {
return super.foo();
}
}

覆盖继承的状态变量

与函数不同,状态变量不能通过在子合约中重新声明来覆盖。

让我们学习如何正确覆盖继承的状态变量。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract A {
string public name = "Contract A";

function getName() public view returns (string memory) {
return name;
}
}

// Solidity 0.6 中不允许使用
// 这不会编译
// contract B is A {
// string public name = "Contract B";
// }

contract C is A {
// 这是覆盖继承的状态变量的正确方法。
constructor() {
name = "Contract C";
}

// C.getName returns "Contract C"
}

调用父合约

可以直接调用父合约,也可以使用关键字 super。

通过使用关键字 super,所有的直接父合约都会被调用。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

/* Inheritance tree
A
/ \
B C
\ /
D
*/

contract A {
// 定义事件,您可以从您的函数发出事件,并将它们记录到事务日志中。 在我们的例子中,这对于跟踪函数调用很有用。
event Log(string message);

function foo() public virtual {
emit Log("A.foo called");
}

function bar() public virtual {
emit Log("A.bar called");
}
}

contract B is A {
function foo() public virtual override {
emit Log("B.foo called");
A.foo();
}

function bar() public virtual override {
emit Log("B.bar called");
super.bar();
}
}

contract C is A {
function foo() public virtual override {
emit Log("C.foo called");
A.foo();
}

function bar() public virtual override {
emit Log("C.bar called");
super.bar();
}
}

contract D is B, C {
// Try:
// - Call D.foo and check the transaction logs.
// Although D inherits A, B and C, it only called C and then A.
// - Call D.bar and check the transaction logs
// D called C, then B, and finally A.
// Although super was called twice (by B and C) it only called A once.

function foo() public override(B, C) {
super.foo();
}

function bar() public override(B, C) {
super.bar();
}
}

Visibility 能见度

函数和状态变量必须声明它们是否可以被其他合约访问。

函数可以声明为

  • public - 任何合约和账户都可以调用
  • private - 仅在定义函数的合约内
  • internal - 仅继承内部功能的内部合同
  • external - 只有其他合约和账户可以调用

状态变量可以声明为公共的、私有的或内部的,但不能声明为外部的。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Base {
// private 私有函数
// - 在当前合约中
function privateFunc() private pure returns (string memory) {
return "private function called";
}

function testPrivateFunc() public pure returns (string memory) {
return privateFunc();
}

// Internal 内部函数的可用范围
// - 在当前合约中
// - 继承此合约的合约内部
function internalFunc() internal pure returns (string memory) {
return "internal function called";
}

function testInternalFunc() public pure virtual returns (string memory) {
return internalFunc();
}

// Public 公共函数的可用范围
// - 在当前合约中
// - 继承此合约的合约内部
// - 通过其他合约和账户调用
function publicFunc() public pure returns (string memory) {
return "public function called";
}

// External 外部函数
// - 通过其他合约和账户调用
function externalFunc() external pure returns (string memory) {
return "external function called";
}

// 这个函数不会编译,因为我们在这里尝试调用一个外部函数。
// function testExternalFunc() public pure returns (string memory) {
// return externalFunc();
// }

// 状态变量
string private privateVar = "my private variable";
string internal internalVar = "my internal variable";
string public publicVar = "my public variable";
// 状态变量不能是外部的,因此此代码无法编译。
// string external externalVar = "my external variable";
}

contract Child is Base {
// 继承的合约无法访问私有函数和状态变量。
// function testPrivateFunc() public pure returns (string memory) {
// return privateFunc();
// }

// 在子合约内部调用Internal内部函数。
function testInternalFunc() public pure override returns (string memory) {
return internalFunc();
}
}

Interface 接口

您可以通过声明接口与其他合约进行交互。

  • 不能实现任何功能
  • 可以从其他接口继承
  • 所有声明的函数必须是外部的
  • 不能声明构造函数
  • 不能声明状态变量
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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Counter {
uint public count;

function increment() external {
count += 1;
}
}

interface ICounter {
function count() external view returns (uint);

function increment() external;
}

contract MyContract {
function incrementCounter(address _counter) external {
ICounter(_counter).increment();
}

function getCount(address _counter) external view returns (uint) {
return ICounter(_counter).count();
}
}

// Uniswap example
interface UniswapV2Factory {
function getPair(address tokenA, address tokenB)
external
view
returns (address pair);
}

interface UniswapV2Pair {
function getReserves()
external
view
returns (
uint112 reserve0,
uint112 reserve1,
uint32 blockTimestampLast
);
}

contract UniswapExample {
address private factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
address private dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address private weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

function getTokenReserves() external view returns (uint, uint) {
address pair = UniswapV2Factory(factory).getPair(dai, weth);
(uint reserve0, uint reserve1, ) = UniswapV2Pair(pair).getReserves();
return (reserve0, reserve1);
}
}

Payable

声明为payable的函数和地址可以在合约中接收以太币。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Payable {
// 收款地址可以接收以太币
address payable public owner;

// Payable 构造函数可以接收以太币
constructor() payable {
owner = payable(msg.sender);
}

// 将以太币存入该合约的函数。
// 与一些 Ether 一起调用此函数。
// 该合约的余额会自动更新。
function deposit() public payable {}

// Call this function along with some Ether.
// The function will throw an error since this function is not payable.
function notPayable() public {}

// 从该合约中提取所有以太币的功能。
function withdraw() public {
// 获取存储在该合约中的以太币数量
uint amount = address(this).balance;

// 将所有以太币发送给所有者
// 所有者可以收到以太币,因为所有者的地址是应付的
(bool success, ) = owner.call{value: amount}("");
require(success, "Failed to send Ether");
}

// 将以太币从这个合约转移到输入地址的函数
function transfer(address payable _to, uint _amount) public {
// 请注意,“to”被声明为 payable
(bool success, ) = _to.call{value: _amount}("");
require(success, "Failed to send Ether");
}
}

发送以太币(转账、发送、调用)

如何发送以太币?

您可以通过以下方式将以太币发送到其他合约

  • transfer (2300 gas, throws error)
  • send (2300 gas, returns bool)
  • call (forward all gas or set gas, returns bool)

如何接收以太币?

接收 Ether 的合约必须至少具有以下功能之一

  • receive() external payable
  • fallback() external payable

如果 msg.data 为空,则调用 receive(),否则调用 fallback()。

您应该使用哪种方法?

call in combination with re-entrancy guard is the recommended method to use after December 2019.

通过以下方式防止重新进入

  • 在调用其他合约之前进行所有状态更改
  • 使用重入保护修饰符
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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract ReceiveEther {
/*
Which function is called, fallback() or receive()?

send Ether
|
msg.data is empty?
/ \
yes no
/ \
receive() exists? fallback()
/ \
yes no
/ \
receive() fallback()
*/

// 接收以太币的功能。 msg.data 必须为空
receive() external payable {}

// 当 msg.data 不为空时调用回退函数
fallback() external payable {}

function getBalance() public view returns (uint) {
return address(this).balance;
}
}

contract SendEther {
function sendViaTransfer(address payable _to) public payable {
// 不再推荐使用此功能发送以太币。
_to.transfer(msg.value);
}

function sendViaSend(address payable _to) public payable {
// Send 返回一个布尔值,指示成功或失败。
// 不建议使用此功能发送 Ether。
bool sent = _to.send(msg.value);
require(sent, "Failed to send Ether");
}

function sendViaCall(address payable _to) public payable {
// Call 返回一个布尔值,指示成功或失败。
// 这是当前推荐使用的方法。
(bool sent, bytes memory data) = _to.call{value: msg.value}("");
require(sent, "Failed to send Ether");
}
}

Fallback

fallback 是一个不接受任何参数且不返回任何内容的函数。

它在何时执行?

  • 调用不存在的函数或
  • 以太币被直接发送到合约,但 receive() 不存在或 msg.data 不为空

当通过 transfer 或 send 调用 fallback 时,gas 限制为 2300。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Fallback {
event Log(uint gas);

// Fallback 函数必须声明为外部函数.
fallback() external payable {
// send / transfer (将 2300 gas 转发到这个后备函数)
// call(转发所有的gas)
emit Log(gasleft());
}

// 辅助函数检查该合约的余额
function getBalance() public view returns (uint) {
return address(this).balance;
}
}

contract SendToFallback {
function transferToFallback(address payable _to) public payable {
_to.transfer(msg.value);
}

function callFallback(address payable _to) public payable {
(bool sent, ) = _to.call{value: msg.value}("");
require(sent, "Failed to send Ether");
}
}

Call

call 是与其他合约交互的低级函数。

当您只是通过调用回退函数发送以太币时,这是推荐的方法。

但是,这不是调用现有函数的推荐方法。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Receiver {
event Received(address caller, uint amount, string message);

fallback() external payable {
emit Received(msg.sender, msg.value, "Fallback was called");
}

function foo(string memory _message, uint _x) public payable returns (uint) {
emit Received(msg.sender, msg.value, _message);

return _x + 1;
}
}

contract Caller {
event Response(bool success, bytes data);

// 假设合约 B 没有合约 A 的源代码,但我们知道 A 的地址和要调用的函数。
function testCallFoo(address payable _addr) public payable {
// 您可以发送以太币并指定自定义gas数量
(bool success, bytes memory data) = _addr.call{value: msg.value, gas: 5000}(
abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)
);

emit Response(success, data);
}

// 调用不存在的函数会触发 fallback 函数.
function testCallDoesNotExist(address _addr) public {
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("doesNotExist()")
);

emit Response(success, data);
}
}

Delegatecall

delegatecall 是一个类似于 call 的低级函数。

当合约A对合约B执行delegatecall时,执行B的代码 使用合约 A 的存储,msg.sender 和 msg.value。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

// 注意:首先部署此合约
contract B {
// 注意:存储布局必须与合约 A 相同
uint public num;
address public sender;
uint public value;

function setVars(uint _num) public payable {
num = _num;
sender = msg.sender;
value = msg.value;
}
}

contract A {
uint public num;
address public sender;
uint public value;

function setVars(address _contract, uint _num) public payable {
// A's storage is set, B is not modified.
(bool success, bytes memory data) = _contract.delegatecall(
abi.encodeWithSignature("setVars(uint256)", _num)
);
}
}

函数选择器

调用函数时,calldata的前 4 个字节指定调用哪个函数。这 4 个字节称为函数选择器。

Take for example, this code below. It uses call to execute transfer on a contract at the address addr.

1
addr.call(abi.encodeWithSignature("transfer(address,uint256)", 0xSomeAddress, 123))

The first 4 bytes returned from abi.encodeWithSignature(....) is the function selector.

Perhaps you can save a tiny amount of gas if you precompute and inline the function selector in your code?

Here is how the function selector is computed.

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

contract FunctionSelector {
/*
"transfer(address,uint256)"
0xa9059cbb
"transferFrom(address,address,uint256)"
0x23b872dd
*/
function getSelector(string calldata _func) external pure returns (bytes4) {
return bytes4(keccak256(bytes(_func)));
}
}

调用其他合约

合约可以通过 2 种方式调用其他合约。

最简单的方法就是直接调用它,比如 A.foo(x, y, z)

调用其他合约的另一种方法是使用低级call。

不推荐这种方法。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Callee {
uint public x;
uint public value;

function setX(uint _x) public returns (uint) {
x = _x;
return x;
}

function setXandSendEther(uint _x) public payable returns (uint, uint) {
x = _x;
value = msg.value;

return (x, value);
}
}

contract Caller {
function setX(Callee _callee, uint _x) public {
uint x = _callee.setX(_x);
}

function setXFromAddress(address _addr, uint _x) public {
Callee callee = Callee(_addr);
callee.setX(_x);
}

function setXandSendEther(Callee _callee, uint _x) public payable {
(uint x, uint value) = _callee.setXandSendEther{value: msg.value}(_x);
}
}

创建其他合约的合约

其他合约可以使用 new 关键字创建合约。从 0.8.0 开始,new 关键字通过指定 salt 选项来支持 create2 功能。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Car {
address public owner;
string public model;
address public carAddr;

constructor(address _owner, string memory _model) payable {
owner = _owner;
model = _model;
carAddr = address(this);
}
}

contract CarFactory {
Car[] public cars;

function create(address _owner, string memory _model) public {
Car car = new Car(_owner, _model);
cars.push(car);
}

function createAndSendEther(address _owner, string memory _model) public payable {
Car car = (new Car){value: msg.value}(_owner, _model);
cars.push(car);
}

function create2(
address _owner,
string memory _model,
bytes32 _salt
) public {
Car car = (new Car){salt: _salt}(_owner, _model);
cars.push(car);
}

function create2AndSendEther(
address _owner,
string memory _model,
bytes32 _salt
) public payable {
Car car = (new Car){value: msg.value, salt: _salt}(_owner, _model);
cars.push(car);
}

function getCar(uint _index)
public
view
returns (
address owner,
string memory model,
address carAddr,
uint balance
)
{
Car car = cars[_index];

return (car.owner(), car.model(), car.carAddr(), address(car).balance);
}
}

Try Catch

try / catch 只能从外部函数调用和合约创建中捕获错误。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

// External contract used for try / catch examples
contract Foo {
address public owner;

constructor(address _owner) {
require(_owner != address(0), "invalid address");
assert(_owner != 0x0000000000000000000000000000000000000001);
owner = _owner;
}

function myFunc(uint x) public pure returns (string memory) {
require(x != 0, "require failed");
return "my func was called";
}
}

contract Bar {
event Log(string message);
event LogBytes(bytes data);

Foo public foo;

constructor() {
// This Foo contract is used for example of try catch with external call
foo = new Foo(msg.sender);
}

// Example of try / catch with external call
// tryCatchExternalCall(0) => Log("external call failed")
// tryCatchExternalCall(1) => Log("my func was called")
function tryCatchExternalCall(uint _i) public {
try foo.myFunc(_i) returns (string memory result) {
emit Log(result);
} catch {
emit Log("external call failed");
}
}

// Example of try / catch with contract creation
// tryCatchNewContract(0x0000000000000000000000000000000000000000) => Log("invalid address")
// tryCatchNewContract(0x0000000000000000000000000000000000000001) => LogBytes("")
// tryCatchNewContract(0x0000000000000000000000000000000000000002) => Log("Foo created")
function tryCatchNewContract(address _owner) public {
try new Foo(_owner) returns (Foo foo) {
// you can use variable foo here
emit Log("Foo created");
} catch Error(string memory reason) {
// catch failing revert() and require()
emit Log(reason);
} catch (bytes memory reason) {
// catch failing assert()
emit LogBytes(reason);
}
}
}

Import

您可以在 Solidity 中导入本地和外部文件。

Local 本地文件

这是我们的文件夹结构。

1
2
├── Import.sol
└── Foo.sol

Foo.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

struct Point {
uint x;
uint y;
}

error Unauthorized(address caller);

function add(uint x, uint y) pure returns (uint) {
return x + y;
}

contract Foo {
string public name = "Foo";
}

Import.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

// import Foo.sol from current directory
import "./Foo.sol";

// import {symbol1 as alias, symbol2} from "filename";
import {Unauthorized, add as func, Point} from "./Foo.sol";

contract Import {
// Initialize Foo.sol
Foo public foo = new Foo();

// Test Foo.sol by getting it's name.
function getFooName() public view returns (string memory) {
return foo.name();
}
}

External 外部文件

您也可以通过简单地复制 url 从 GitHub 导入

1
2
3
4
5
6
// https://github.com/owner/repo/blob/branch/path/to/Contract.sol
import "https://github.com/owner/repo/blob/branch/path/to/Contract.sol";

// Example import ECDSA.sol from openzeppelin-contract repo, release-v4.5 branch
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";

Library

库类似于合约,但不能声明任何状态变量,也不能发送以太币。

如果所有库函数都是内部的,则将库嵌入到合约中。

否则,必须先部署库,然后在部署合约之前进行链接。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

library SafeMath {
function add(uint x, uint y) internal pure returns (uint) {
uint z = x + y;
require(z >= x, "uint overflow");

return z;
}
}

library Math {
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
// else z = 0 (default value)
}
}

contract TestSafeMath {
using SafeMath for uint;

uint public MAX_UINT = 2**256 - 1;

function testAdd(uint x, uint y) public pure returns (uint) {
return x.add(y);
}

function testSquareRoot(uint x) public pure returns (uint) {
return Math.sqrt(x);
}
}

// Array function to delete element at index and re-organize the array
// so that their are no gaps between the elements.
library Array {
function remove(uint[] storage arr, uint index) public {
// Move the last element into the place to delete
require(arr.length > 0, "Can't remove from empty array");
arr[index] = arr[arr.length - 1];
arr.pop();
}
}

contract TestArray {
using Array for uint[];

uint[] public arr;

function testArrayRemove() public {
for (uint i = 0; i < 3; i++) {
arr.push(i);
}

arr.remove(1);

assert(arr.length == 2);
assert(arr[0] == 0);
assert(arr[1] == 2);
}
}

ABI Decode

abi.encode 将数据编码为 bytes.

abi.decodebytes 解码回数据.

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract AbiDecode {
struct MyStruct {
string name;
uint[2] nums;
}

function encode(
uint x,
address addr,
uint[] calldata arr,
MyStruct calldata myStruct
) external pure returns (bytes memory) {
return abi.encode(x, addr, arr, myStruct);
}

function decode(bytes calldata data)
external
pure
returns (
uint x,
address addr,
uint[] memory arr,
MyStruct memory myStruct
)
{
// (uint x, address addr, uint[] memory arr, MyStruct myStruct) = ...
(x, addr, arr, myStruct) = abi.decode(data, (uint, address, uint[], MyStruct));
}
}

Keccak256

keccak256 计算输入的 Keccak-256 哈希

一些使用场景是:

  • 从输入创建确定性唯一 ID
  • 提交-显示方案
  • 紧凑的加密签名(通过签名散列而不是更大的输入)
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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract HashFunction {
function hash(
string memory _text,
uint _num,
address _addr
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_text, _num, _addr));
}

// Example of hash collision
// Hash collision can occur when you pass more than one dynamic data type
// to abi.encodePacked. In such case, you should use abi.encode instead.
function collision(string memory _text, string memory _anotherText)
public
pure
returns (bytes32)
{
// encodePacked(AAA, BBB) -> AAABBB
// encodePacked(AA, ABBB) -> AAABBB
return keccak256(abi.encodePacked(_text, _anotherText));
}
}

contract GuessTheMagicWord {
bytes32 public answer =
0x60298f78cc0b47170ba79c10aa3851d7648bd96f2f8e46a19dbc777c36fb0c00;

// Magic word is "Solidity"
function guess(string memory _word) public view returns (bool) {
return keccak256(abi.encodePacked(_word)) == answer;
}
}

验证签名

消息可以在链下签名,然后使用智能合约在链上进行验证。

Example using ethers.js

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

/* Signature Verification

How to Sign and Verify
# Signing
1. Create message to sign
2. Hash the message
3. Sign the hash (off chain, keep your private key secret)

# Verify
1. Recreate hash from the original message
2. Recover signer from signature and hash
3. Compare recovered signer to claimed signer
*/

contract VerifySignature {
/* 1. Unlock MetaMask account
ethereum.enable()
*/

/* 2. Get message hash to sign
getMessageHash(
0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C,
123,
"coffee and donuts",
1
)

hash = "0xcf36ac4f97dc10d91fc2cbb20d718e94a8cbfe0f82eaedc6a4aa38946fb797cd"
*/
function getMessageHash(
address _to,
uint _amount,
string memory _message,
uint _nonce
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_to, _amount, _message, _nonce));
}

/* 3. Sign message hash
# using browser
account = "copy paste account of signer here"
ethereum.request({ method: "personal_sign", params: [account, hash]}).then(console.log)

# using web3
web3.personal.sign(hash, web3.eth.defaultAccount, console.log)

Signature will be different for different accounts
0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
*/
function getEthSignedMessageHash(bytes32 _messageHash)
public
pure
returns (bytes32)
{
/*
Signature is produced by signing a keccak256 hash with the following format:
"\x19Ethereum Signed Message\n" + len(msg) + msg
*/
return
keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
);
}

/* 4. Verify signature
signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd
to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C
amount = 123
message = "coffee and donuts"
nonce = 1
signature =
0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
*/
function verify(
address _signer,
address _to,
uint _amount,
string memory _message,
uint _nonce,
bytes memory signature
) public pure returns (bool) {
bytes32 messageHash = getMessageHash(_to, _amount, _message, _nonce);
bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);

return recoverSigner(ethSignedMessageHash, signature) == _signer;
}

function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature)
public
pure
returns (address)
{
(bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);

return ecrecover(_ethSignedMessageHash, v, r, s);
}

function splitSignature(bytes memory sig)
public
pure
returns (
bytes32 r,
bytes32 s,
uint8 v
)
{
require(sig.length == 65, "invalid signature length");

assembly {
/*
First 32 bytes stores the length of the signature

add(sig, 32) = pointer of sig + 32
effectively, skips first 32 bytes of signature

mload(p) loads next 32 bytes starting at the memory address p into memory
*/

// first 32 bytes, after the length prefix
r := mload(add(sig, 32))
// second 32 bytes
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}

// implicitly return (r, s, v)
}
}