其中solidity是一门专门用于编写智能合约的高级语言,在以太坊虚拟机上运行。它是一种静态类型语言,受 JavaScript、Python 和 C++ 的影响,具有合约、状态变量、函数修饰符和事件等概念,适用于去中心化应用(DApp)和区块链开发。

智能合约的概念

智能合约(Smart Contract)是一种运行在区块链上的自执行合约,其条款以代码形式直接写入区块链。一旦满足设定的条件,合约便会自动执行,且不可篡改。智能合约通常用于去中心化金融(DeFi)、NFT、供应链管理等场景,以实现信任最小化和自动化交易。

1.变量类型

常用三种类型:

1.值类型: 布尔型(bool),整数型(int),正整数(uint)等

2.引用类型: 数组(arry[]),结构体(struct)等

3.映射类型:存储键值对的数据结构(mapping)

详细介绍

1布尔型

布尔型是二值变量,取值为 truefalse

1
bool san = ture;

布尔值的运算符包括:

  • ! (逻辑非)
  • && (逻辑与,”and”)
  • || (逻辑或,”or”)
  • == (等于)
  • != (不等于)

2 整型

solidity中的整数

1
2
3
int public _int = -1; // 整数,包括负数
uint public _uint = 1; // 正整数
uint256 public _number = 20220330; // 256位正整数

常用的整型运算符包括:

  • 比较运算符(返回布尔值): <=<==!=>=>
  • 算数运算符: +-*/%(取余),**(幂)
1
2
3
4
5
//整数运算
uint256 public _number1 = _number + 1; // +,-,*,/
uint256 public _number2 = 2**2; // 指数
uint256 public _number3 = 7 % 2; // 取余数
bool public _numberbool = _number2 > _number3; // 比大小

3.地址类型

其中分为两种类型:

  • 普通地址(address): 存储一个 20 字节的值(以太坊地址的大小)。

  • payable address : 比普通地址多了 transfersend 两个成员方法,用于接收转账。

4.字长字节数组

字节数组分为定长和不定长两种:

  • 定长字节数组: 属于值类型,数组长度在声明之后不能改变。根据字节数组的长度分为 bytes1, bytes8, bytes32 等类型。定长字节数组最多存储 32 bytes 数据,即bytes32
  • 不定长字节数组: 属于引用类型数组长度在声明之后可以改变,包括 bytes 等(动态数组)

2.函数

四种函数

  • 公共函数

  • 内部函数

  • 外部函数

  • 私有函数

详细介绍

1.公共函数

特点: 可以从合约外部和内部调用,默认可见修饰符。

调用方式: 可以直接在合约内调用。可以从合约外部通过合约地址调用。

限制: 无限制

继承:可以被继承并在子合约中重写

1
2
3
4
5
6
contract Example {    
function publicFunction() public pure returns (string memory) {
return "This is a public function"; }
function callPublicFunction() public pure returns (string memory) {
return publicFunction(); // 内部调用 }
} // 外部调用 Example example = new Example(); example.publicFunction();

2.内部函数

特点:只能在当前合约和从当前合约继承的合约中调用,比public函数更加高效,因为他们不会涉及外部调用的开销。

**调用方式:**只能在合约内或继承合约中调用

**限制:**不能从合约外部调用

**继承:**可以被继承并且子合约中调用或被重写

1
2
3
4
5
6
7
8
9
10
contract Parent {    
function internalFunction() internal pure returns (string memory) {
return "This is an internal function"; }
function callInternalFunction() public pure returns (string memory) {
return internalFunction(); // 内部调用 }
}
contract Child is Parent {
function callParentInternalFunction() public pure returns (string memory) {
return internalFunction(); // 继承合约中调用 }
}

3.外部函数

**特点:**只能从合约外部调用。可以通过this关键子从合约外部调用,但是效率低。

**调用方式:**从合约外部通过合约地址调用,使用this关键字调用

**限制:**不能直接在合约内部调用

**继承:**可以被继承,并在子合约中重写

1
2
3
4
5
6
contract Example {    
function externalFunction() external pure returns (string memory) {
return "This is an external function"; }
function callExternalFunction() public view returns (string memory) {
return this.externalFunction(); // 使用 `this` 关键字调用 }
} // 外部调用 Example example = new Example(); example.externalFunction();

4.私有函数

**特点:**只能在当前合约内部调用,访问权限最严格。

**调用方式:**只能在合约内部调用

**限制:**不能从合约外部或继承合约中调用。

**继承:**不能被继承,子合约无法访问或者重写。

1
2
3
4
5
6
contract Example {    
function privateFunction() private pure returns (string memory) {
return "This is a private function"; }
function callPrivateFunction() public pure returns (string memory) {
return privateFunction(); // 内部调用 }
} // 无法从外部或继承合约中调用

总结

public: 可在任何地方调用,包括合约内部和外部,并可继承。

internal: 只能在当前合约和继承合约中调用,并可继承。

external: 只能从合约外部调用,可通过 this 关键字在内部调用,并可继承。

private: 只能在当前合约内部调用,不能继承。

3.控制流

if语句

1语法:

1
2
3
4
if (条件) {
// 当条件为 true 时执行的代码
}

2.if-else 语句

1
2
3
4
5
6
if (条件) {
// 条件为 true 时执行
} else {
// 条件为 false 时执行
}

3. if-else if-else 语句

1
2
3
4
5
6
7
8
if (条件1) {
// 条件1 为 true 时执行
} else if (条件2) {
// 条件2 为 true 时执行
} else {
// 所有条件都不满足时执行
}

for循环(以及其他循环)

1.语法

1
2
3
4
for (初始化; 条件; 迭代) {
// 代码块
}

2.while循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.8.0;

contract WhileLoopExample {
function sum(uint256 n) public pure returns (uint256) {
uint256 total = 0;
uint256 i = 1;
while (i <= n) {
total += i;
i++;
}
return total;
}
}

3.do-while 循环

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.8.0;

contract DoWhileExample {
function decrement(uint256 n) public pure returns (uint256) {
uint256 counter = n;
do {
counter--;
} while (counter > 0);
return counter;
}
}

  • do-while 至少会执行一次,即使初始条件不满足。

4继承

继承规则

  • virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。
  • override:子合约重写了父合约中的函数,需要加上override关键字。

注意:用override修饰public变量,会重写与变量同名的getter函数,例如:

1
mapping(address => uint256) public override balanceOf;

简单继承

我们先写一个简单的爷爷合约Yeye,里面包含1个Log事件和3个function: hip(), pop(), yeye(),输出都是”Yeye”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
contract Yeye {
event Log(string msg);

// 定义3个function: hip(), pop(), man(),Log值为Yeye。
function hip() public virtual{
emit Log("Yeye");
}

function pop() public virtual{
emit Log("Yeye");
}

function yeye() public virtual {
emit Log("Yeye");
}
}

我们再定义一个爸爸合约Baba,让他继承Yeye合约,语法就是contract Baba is Yeye,非常直观。在Baba合约里,我们重写一下hip()pop()这两个函数,加上override关键字,并将他们的输出改为”Baba”;并且加一个新的函数baba,输出也是”Baba”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract Baba is Yeye{
// 继承两个function: hip()和pop(),输出改为Baba。
function hip() public virtual override{
emit Log("Baba");
}

function pop() public virtual override{
emit Log("Baba");
}

function baba() public virtual{
emit Log("Baba");
}
}

我们部署合约,可以看到Baba合约里有4个函数,其中hip()pop()的输出被成功改写成”Baba”,而继承来的yeye()的输出仍然是”Yeye”

多重继承

Solidity的合约可以继承多个合约。

规则:

  1. 继承时要按辈分最高到最低的顺序排。比如我们写一个Erzi合约,继承Yeye合约和Baba合约,那么就要写成contract Erzi is Yeye, Baba,而不能写成contract Erzi is Baba, Yeye,不然就会报错。(即按照合约的复杂程度大小排序)
  2. 如果某一个函数在多个继承的合约里都存在,比如例子中的hip()pop(),在子合约里必须重写,不然会报错。
  3. 重写在多个父合约中都重名的函数时,override关键字后面要加上所有父合约名字,例如override(Yeye, Baba)
1
2
3
4
5
6
7
8
9
10
contract Erzi is Yeye, Baba{
// 继承两个function: hip()和pop(),输出值为Erzi。
function hip() public virtual override(Yeye, Baba){
emit Log("Erzi");
}

function pop() public virtual override(Yeye, Baba) {
emit Log("Erzi");
}
}

我们可以看到,Erzi合约里面重写了hip()pop()两个函数,将输出改为”Erzi”,并且还分别从YeyeBaba合约继承了yeye()baba()两个函数。

修饰器的继承

Solidity中的修饰器(Modifier)同样可以继承,用法与函数继承类似,在相应的地方加virtualoverride关键字即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
contract Base1 {
modifier exactDividedBy2And3(uint _a) virtual {
require(_a % 2 == 0 && _a % 3 == 0);
_;
}
}

contract Identifier is Base1 {

//计算一个数分别被2除和被3除的值,但是传入的参数必须是2和3的倍数
function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) {
return getExactDividedBy2And3WithoutModifier(_dividend);
}

//计算一个数分别被2除和被3除的值
function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){
uint div2 = _dividend / 2;
uint div3 = _dividend / 3;
return (div2, div3);
}
}

Identifier合约可以直接在代码中使用父合约中的exactDividedBy2And3修饰器,也可以利用override关键字重写修饰器:

1
2
3
4
modifier exactDividedBy2And3(uint _a) override {
_;
require(_a % 2 == 0 && _a % 3 == 0);
}

构造函数的继承

子合约有两种方法继承父合约的构造函数。举个简单的例子,父合约A里面有一个状态变量a,并由构造函数的参数来确定:

1
2
3
4
5
6
7
8
// 构造函数的继承
abstract contract A {
uint public a;

constructor(uint _a) {
a = _a;
}
}
  1. 在继承时声明父构造函数的参数,例如:contract B is A(1)

  2. 在子合约的构造函数中声明构造函数的参数,例如:

    1
    2
    3
    contract C is A {
    constructor(uint _c) A(_c * _c) {}
    }

调用父合约的函数

子合约有两种方式调用父合约的函数,直接调用和利用super关键字。

  1. 直接调用:子合约可以直接用父合约名.函数名()的方式来调用父合约函数,例如Yeye.pop()

    1
    2
    3
    function callParent() public{
    Yeye.pop();
    }
  2. super关键字:子合约可以利用super.函数名()来调用最近的父合约函数。Solidity继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba,那么Baba是最近的父合约,super.pop()将调用Baba.pop()而不是Yeye.pop()

    1
    2
    3
    4
    function callParentSuper() public{
    // 将调用最近的父合约函数,Baba.pop()
    super.pop();
    }

钻石继承

在面向对象编程中,钻石继承(菱形继承)指一个派生类同时有两个或两个以上的基类。

在多重+菱形继承链条上使用super关键字时,需要注意的是使用super会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约。

我们先写一个合约God,再写AdamEve两个合约继承God合约,最后让创建合约people继承自AdamEve,每个合约都有foobar两个函数。

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;

/* 继承树:
God
/ \
Adam Eve
\ /

people
*/

contract God {
event Log(string message);

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

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

contract Adam is God {
function foo() public virtual override {
emit Log("Adam.foo called");
super.foo();
}

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

contract Eve is God {
function foo() public virtual override {
emit Log("Eve.foo called");
super.foo();
}

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

contract people is Adam, Eve {
function foo() public override(Adam, Eve) {
super.foo();
}

function bar() public override(Adam, Eve) {
super.bar();
}
}

在这个例子中,调用合约people中的super.bar()会依次调用EveAdam,最后是God合约。

5.抽象合约

抽象合约

如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体{}中的内容,则必须将该合约标为abstract,不然编译会报错;另外,未实现的函数需要加virtual,以便子合约重写。拿我们之前的插入排序合约为例,如果我们还没想好具体怎么实现插入排序函数,那么可以把合约标为abstract,之后让别人补写上。

1
2
3
abstract contract InsertionSort{
function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);
}

6.接口

  1. 不能包含状态变量
  2. 不能包含构造函数
  3. 不能继承除接口外的其他合约
  4. 所有函数都必须是external且不能有函数体
  5. 继承接口的非抽象合约必须实现接口定义的所有功能

作用

接口是智能合约的骨架,定义合约的功能以如何触发他们,

如果智能合约实现了某种接口(比如ERC20ERC721),其他Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息:

  1. 合约里每个函数的bytes4选择器,以及函数签名函数名(每个参数类型)
  2. 接口id

另外,接口与合约ABI(Application Binary Interface)等价,可以相互转换:编译接口可以得到合约的ABI,利用abi-to-sol工具,也可以将ABI json文件转换为接口sol文件。

我们以ERC721接口合约IERC721为例,它定义了3个event和9个function,所有ERC721标准的NFT都实现了这些函数。我们可以看到,接口和常规合约的区别在于每个函数都以;代替函数体{ }结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface IERC721 is IERC165 {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

function balanceOf(address owner) external view returns (uint256 balance);

function ownerOf(uint256 tokenId) external view returns (address owner);

function safeTransferFrom(address from, address to, uint256 tokenId) external;

function transferFrom(address from, address to, uint256 tokenId) external;

function approve(address to, uint256 tokenId) external;

function getApproved(uint256 tokenId) external view returns (address operator);

function setApprovalForAll(address operator, bool _approved) external;

function isApprovedForAll(address owner, address operator) external view returns (bool);

function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external;
}

什么时候使用接口?

如果我们知道一个合约实现了IERC721接口,我们不需要知道它具体代码实现,就可以与它交互。

无聊猿BAYC属于ERC721代币,实现了IERC721接口的功能。我们不需要知道它的源代码,只需知道它的合约地址,用IERC721接口就可以与它交互,比如用balanceOf()来查询某个地址的BAYC余额,用safeTransferFrom()来转账BAYC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract interactBAYC {
// 利用BAYC地址创建接口合约变量(ETH主网)
IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);

// 通过接口调用BAYC的balanceOf()查询持仓量
function balanceOfBAYC(address owner) external view returns (uint256 balance){
return BAYC.balanceOf(owner);
}

// 通过接口调用BAYC的safeTransferFrom()安全转账
function safeTransferFromBAYC(address from, address to, uint256 tokenId) external{
BAYC.safeTransferFrom(from, to, tokenId);
}
}

抽象合约的例子:

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
// 定义一个抽象合约 Animal
abstract contract Animal {
// 声明一个未实现的函数
function makeSound() public virtual returns (string memory);


// 可以有其他实现的函数
function move() public virtual returns (string memory) {
return "Moving...";
}

}

// 具体实现的合约 Dog
contract Dog is Animal {
function makeSound() public override returns (string memory) {
return "Woof!";
}
}

// 具体实现的合约 Cat
contract Cat is Animal {
function makeSound() public override returns (string memory) {
return "Meow!";
}
}

Animal 是一个抽象合约,使用 abstract 关键字声明。

Animal 中声明了一个未实现的函数 makeSound(),以及一个有默认实现的函数 move()

DogCat 合约分别继承了 Animal 抽象合约,并实现了 makeSound() 函数,分别返回了狗和猫的叫声。

接口的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义一个接口 Animal
interface Animal {
function makeSound() external returns (string memory);
}

// 具体实现的合约 Dog
contract Dog is Animal {
function makeSound() public override returns (string memory) {
return "Woof!";
}
}

// 具体实现的合约 Cat
contract Cat is Animal {
function makeSound() public override returns (string memory) {
return "Meow!";
}
}

在这个示例中,Animal 是一个接口,声明了一个函数 makeSound(),没有提供具体的实现。DogCat 合约分别实现了 Animal 接口,并提供了 makeSound() 函数的具体实现,分别返回狗和猫的叫声。

区别和使用场景:

  • 抽象合约通常用于定义具有部分实现的合约,可以包含实现的函数和未实现的函数。
  • 接口是完全未实现的合约,用于定义函数的签名和行为,强制要求实现合约必须提供具体的实现。

7 事件

当触发事件的时候有俩种参数类型:

  • indexed参数
  • 非indexed参数

最多可以有三个indexed参数 ,也被称为topics(主题);indexed只能用于 uint,address类型的变量;

主题 topics

日志的第一部分是主题数组,用于描述事件,长度不能超过4。它的第一个元素是事件的签名(哈希)。对于上面的Transfer事件,它的事件哈希就是:

1
2
3
keccak256("Transfer(address,address,uint256)")

//0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

除了事件哈希,主题还可以包含至多3indexed参数,也就是Transfer事件中的fromto

indexed标记的参数可以理解为检索事件的索引“键”,方便之后搜索。每个 indexed 参数的大小为固定的256比特,如果参数太大了(比如字符串),就会自动计算哈希存储在主题中。

数据 data

事件中不带 indexed的参数会被存储在 data 部分中,可以理解为事件的“值”。data 部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 data 部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 topics 部分中,也是以哈希的方式存储。另外,data 部分的变量在存储上消耗的gas相比于 topics 更少。

**简介:**solidity 中事件是EVM上日志的抽象:

**响应:**应用程序(ethers.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。

**经济:**事件相对于一个新变量更加经济,(一个事件大概2000gas,新变量一个要20000gas)

**声明时间:**由event开头,后面是时间的名称,在括号里面是(变量类型和变量名)

例子:

1
event Transfer(address indexed from, address indexed to, uint256 value);

释放事件

我们可以在函数里释放事件。在下面的例子中,每次用_transfer()函数进行转账操作的时候,都会释放Transfer事件,并记录相应的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义_transfer函数,执行转账逻辑
function _transfer(
address from,
address to,
uint256 amount
) external {

_balances[from] = 10000000; // 给转账地址一些初始代币

_balances[from] -= amount; // from地址减去转账数量
_balances[to] += amount; // to地址加上转账数量

// 释放事件
emit Transfer(from, to, amount);//可以受用eimt来释放事件
}

8.日志

日志(Event)是 Solidity 语言提供的一个重要功能,用于在 EVM 内部记录区块链上的事件。Event 主要用于在链下监听智能合约的状态变化,减少对区块链状态的频繁查询,提高效率。日志由 EVM 处理,并存储在交易的 logs 部分,智能合约本身无法直接访问。

1特点

存储在交易日志中,不占用合约存储空间:事件数据不会占用 storage,因此 gas 费用更低。

无法在智能合约内部读取:Event 仅用于链外监听(off-chain),不能在智能合约内直接调用。

可以携带索引(indexed):允许最多 三个 indexed 参数,方便链外工具(如 Web3.js、ethers.js)高效检索。

用于链上事件通知:DApp 开发中,前端可以监听事件,以响应合约执行状态的变化。

2.日志的声明与使用

在 Solidity 中,日志通过 event 关键字定义,并使用 emit 关键字触发。

2.1 定义日志

1
2
solidity复制编辑// 定义一个事件,记录转账信息
event Transfer(address indexed from, address indexed to, uint256 value);

说明:

  • Transfer
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    事件包含 3 个参数:

    - `from`(`indexed`):发送方地址
    - `to`(`indexed`):接收方地址
    - `value`(未 `indexed`):转账金额

    - `indexed` 修饰符允许事件数据被高效索引,最多 3 个参数可以使用 `indexed`。

    ------

    #### 2.2 **触发事件**

    ```solidity
    solidity复制编辑function transfer(address _to, uint256 _amount) public {
    require(balance[msg.sender] >= _amount, "Insufficient balance");
    balance[msg.sender] -= _amount;
    balance[_to] += _amount;

    // 触发 Transfer 事件
    emit Transfer(msg.sender, _to, _amount);
    }

说明:

  • 事件 Transferemit 关键字触发,并记录在交易日志中。
  • 监听器(如前端)可以监听该事件,并更新 UI。

9.内置函数

1 数学运算相关

函数 作用
addmod(uint x, uint y, uint m) 计算 (x + y) % m,避免溢出
mulmod(uint x, uint y, uint m) 计算 (x * y) % m,避免溢出

2.哈希计算

函数 作用
keccak256(bytes memory data) 计算 Keccak-256 哈希值
sha256(bytes memory data) 计算 SHA-256 哈希值
ripemd160(bytes memory data) 计算 RIPEMD-160 哈希值

3.地址相关

函数 作用
address(uint160 value) uint160转换为address
uint160(address value) address转换为uint160
balance 获取账户余额
send(uint256 amount) 发送以太币(返回 bool
transfer(uint256 amount) 发送以太币(失败时回滚)
call(bytes memory data) 低级调用外部合约

4. 区块和交易信息

函数 作用
block.number 当前区块号
block.timestamp 当前区块的时间戳
block.coinbase 当前区块的矿工地址
block.difficulty 当前区块的难度
gasleft() 剩余的 gas 量
msg.sender 交易发送者地址
msg.value 交易发送的以太币数量
msg.data 调用数据
tx.origin 交易发起者的地址

5 ABI 编码和解码

函数 作用
abi.encode(...) 编码参数为 bytes
abi.encodePacked(...) 压缩编码(节省空间)
abi.decode(bytes memory data, (type1, type2, ...)) 解码数据
abi.encodeWithSelector(bytes4 selector, ...) 编码带 selector 的调用数据
abi.encodeWithSignature(string memory signature, ...) 编码带函数签名的调用数据

6. 合约创建

函数 作用
new ContractName{value: X, gas: Y}(params) 部署新合约
selfdestruct(address payable recipient) 销毁合约,并转移剩余资金

10 以太坊的账户(转账方法

其中以太坊账户分为两类:

  1. 外部账户(EOA,Externally Owned Account)
    • 由私钥控制。
    • 通过 msg.sender 发送交易。
    • 不能存储代码,仅能发送和接收以太币。
  2. 合约账户(Contract Account)
    • 由智能合约代码控制。
    • 具有存储和代码逻辑。
    • 由外部账户或合约调用执行。

send call transfer 对比

方法 失败时 Gas 限制 是否推荐
send() 返回 false 2300 不推荐(可能导致资金丢失)
transfer() 回滚交易 2300 推荐(安全性较高)
call() 返回 (success, data) 无限制 推荐(但需注意重入攻击)

send()

send() 方法返回 bool,需要手动检查成功与否:

1
2
(bool success,) = payableAddr.send(1 ether);
require(success, "Send failed");

⚠️ 不推荐使用,因为它不会自动回滚交易。

transfer()

transfer() 方法失败时会自动回滚:

1
payableAddr.transfer(1 ether);

推荐使用,因为它避免了资金丢失。

call()

call() 是最低级的调用方式,可以传递数据,但容易引发重入攻击:

1
2
(bool success, bytes memory data) = payableAddr.call{value: 1 ether}("");
require(success, "Call failed");

⚠️ 推荐 用于复杂合约交互,但需加 reentrancy 保护。(防止重入)

11. 映射(mapping)

mapping 是 Solidity 中的一种哈希表,键值对存储数据,但 无法遍历获取长度

mapping 语法

1
2
solidity复制编辑mapping(address => uint256) public balances;
mapping(uint256 => mapping(address => bool)) public approvals; // 多级映射

基本操作

1
2
3
4
solidity复制编辑balances[msg.sender] = 100; // 赋值
uint256 bal = balances[msg.sender]; // 读取
bool isApproved = approvals[1][msg.sender]; // 读取多级映射
delete balances[msg.sender]; // 删除(重置为默认值)

12.数组(Array)

Solidity 数组有 固定长度动态数组 两种。

数组定义

1
2
uint256[] public dynamicArray; // 动态数组
uint256[5] public fixedArray; // 固定长度数组

增删查改

1
2
3
4
5
dynamicArray.push(10); // 添加元素
uint256 value = dynamicArray[0]; // 读取
dynamicArray[0] = 20; // 修改
delete dynamicArray[0]; // 删除(重置为默认值 0)
dynamicArray.pop(); // 删除最后一个元素(仅限动态数组)

注意:固定长度数组不能 push()pop()

13.delete 关键字

  • delete 关键字不会真正删除元素,而是 重置为默认值
  • 适用于数组、mapping 和结构体。
1
2
3
4
5
uint256[] public arr = [1, 2, 3];

function resetArray() public {
delete arr[1]; // 变为 0,数组长度不变
}

对于 mapping

1
2
3
4
5
mapping(address => uint256) public balances;

function clearBalance(address user) public {
delete balances[user]; // 重置为 0
}

14.receive 和 fallback

接收ETH的receive函数

其中receive函数实在接收ETH转账时被调用的函数。一个合约最多有一个receive()函数,声明方式直接为receive() external payable { … }。不需要function关键字。receive()函数不能有任何参数,不能有任何返回值,必须包含external和payable。

接受ETH时候, receive()会被触发。receive()最好不要执行太多的逻辑因为如果别人用sendtransfer方法发送ETH的话,gas会限制在2300receive()太复杂可能会触发Out of Gas报错;如果用call就可以自定义gas`执行更复杂的逻辑(这三种发送ETH的方法我们之后会讲到)。

我们可以在receive()里发送一个event,例如:

1
2
3
4
5
6
// 定义事件
event Received(address Sender, uint Value);
// 接收ETH时释放Received事件
receive() external payable {
emit Received(msg.sender, msg.value);
}

回退函数 fallback

fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contractfallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { ... }

我们定义一个fallback()函数,被触发时候会释放fallbackCalled事件,并输出msg.sendermsg.valuemsg.data:

1
2
3
4
5
6
event fallbackCalled(address Sender, uint Value, bytes Data);

// fallback
fallback() external payable{
emit fallbackCalled(msg.sender, msg.value, msg.data);
}

两者区别

触发fallback() 还是 receive()?
接收ETH
|
msg.data是空?
/
是 否
/
receive()存在? fallback()
/
是 否
/
receive() fallback()

简单来说,合约接收ETH时,msg.data为空且存在receive()时,会触发receive()msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable

receive()payable fallback()均不存在的时候,向合约直接发送ETH将会报错(你仍可以通过带有payable的函数向合约发送ETH

15 构造函数和修饰器

构造函数

构造函数(constructor)是一种特殊的函数,每个合约可以定义一个,并在部署合约的时候自动运行一次。它可以用来初始化合约的一些参数,例如初始化合约的owner地址:

1
2
3
4
5
6
address owner; // 定义owner变量

// 构造函数
constructor(address initialOwner) {
owner = initialOwner; // 在部署合约的时候,将owner设置为传入的initialOwner地址
}

注意:构造函数在不同的Solidity版本中的语法并不一致,在Solidity 0.4.22之前,构造函数不使用 constructor 而是使用与合约名同名的函数作为构造函数而使用,由于这种旧写法容易使开发者在书写时发生疏漏(例如合约名叫 Parents,构造函数名写成 parents),使得构造函数变成普通函数,引发漏洞,所以0.4.22版本及之后,采用了全新的 constructor 写法。

构造函数的旧写法代码示例:

1
2
3
4
5
6
pragma solidity =0.4.21;
contract Parents {
// 与合约名Parents同名的函数就是构造函数
function Parents () public {
}
}

修饰器

修饰器(modifier)是Solidity特有的语法,类似于面向对象编程中的装饰器(decorator),声明函数拥有的特性,并减少代码冗余。它就像钢铁侠的智能盔甲,穿上它的函数会带有某些特定的行为。modifier的主要使用场景是运行函数前的检查,例如地址,变量,余额等。

我们来定义一个叫做onlyOwner的modifier:

1
2
3
4
5
// 定义modifier
modifier onlyOwner {
require(msg.sender == owner); // 检查调用者是否为owner地址
_; // 如果是的话,继续运行函数主体;否则报错并revert交易
}

带有onlyOwner修饰符的函数只能被owner地址调用,比如下面这个例子:

1
2
3
function changeOwner(address _newOwner) external onlyOwner{
owner = _newOwner; // 只有owner地址运行这个函数,并改变owner
}

我们定义了一个changeOwner函数,运行它可以改变合约的owner,但是由于onlyOwner修饰符的存在,只有原先的owner可以调用,别人调用就会报错。这也是最常用的控制智能合约权限的方法。

16. 函数重载

重载:

Solidity中允许函数进行重载(overloading),即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。注意,Solidity不允许修饰器(modifier)重载。

函数重载

举个例子,我们可以定义两个都叫saySomething()的函数,一个没有任何参数,输出"Nothing";另一个接收一个string参数,输出这个string

1
2
3
4
5
6
7
function saySomething() public pure returns(string memory){
return("Nothing");
}

function saySomething(string memory something) public pure returns(string memory){
return(something);
}

最终重载函数在经过编译器编译后,由于不同的参数类型,都变成了不同的函数选择器(selector)

17. 库合约

特点(库合约是一种特殊的合约,为了提升Solidity代码的复用性和减少gas而存在。库合约是一系列的函数合集,由大神或者项目方创作,咱们站在巨人的肩膀上,会用就行了。)

和普通合约的不同点:

  1. 不能存在状态变量

  2. 不能够继承或被继承

  3. 不能接收以太币

  4. 不可以被销毁

    Strings库合约

    Strings库合约是将uint256类型转换为相应的string类型的代码库,样例代码如下:

    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
    library Strings {
    bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";

    /**
    * @dev Converts a `uint256` to its ASCII `string` decimal representation.
    */
    function toString(uint256 value) public pure returns (string memory) {
    // Inspired by OraclizeAPI's implementation - MIT licence
    // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

    if (value == 0) {
    return "0";
    }
    uint256 temp = value;
    uint256 digits;
    while (temp != 0) {
    digits++;
    temp /= 10;
    }
    bytes memory buffer = new bytes(digits);
    while (value != 0) {
    digits -= 1;
    buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
    value /= 10;
    }
    return string(buffer);
    }

    /**
    * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
    */
    function toHexString(uint256 value) public pure returns (string memory) {
    if (value == 0) {
    return "0x00";
    }
    uint256 temp = value;
    uint256 length = 0;
    while (temp != 0) {
    length++;
    temp >>= 8;
    }
    return toHexString(value, length);
    }

    /**
    * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
    */
    function toHexString(uint256 value, uint256 length) public pure returns (string memory) {
    bytes memory buffer = new bytes(2 * length + 2);
    buffer[0] = "0";
    buffer[1] = "x";
    for (uint256 i = 2 * length + 1; i > 1; --i) {
    buffer[i] = _HEX_SYMBOLS[value & 0xf];
    value >>= 4;
    }
    require(value == 0, "Strings: hex length insufficient");
    return string(buffer);
    }
    }

    他主要包含两个函数,toString()uint256转为stringtoHexString()uint256转换为16进制,在转换为string

如何使用库合约

我们用Strings库合约的toHexString()来演示两种使用库合约中函数的办法。

  1. 利用using for指令

    指令using A for B;可用于附加库合约(从库 A)到任何类型(B)。添加完指令后,库A中的函数会自动添加为B类型变量的成员,可以直接调用。注意:在调用的时候,这个变量会被当作第一个参数传递给函数:

    1
    2
    3
    4
    5
    6
    // 利用using for指令
    using Strings for uint256;
    function getString1(uint256 _number) public pure returns(string memory){
    // 库合约中的函数会自动添加为uint256型变量的成员
    return _number.toHexString();
    }
  2. 通过库合约名称调用函数

    1
    2
    3
    4
    // 直接通过库合约名调用
    function getString2(uint256 _number) public pure returns(string memory){
    return Strings.toHexString(_number);
    }

    总结:

    会用大神写的就可以了。我们只需要知道什么情况该用什么库合约。常用的有:

    1. Strings:将uint256转换为String

    2. Address:判断某个地址是否为合约地址

    3. Create2:更安全的使用Create2 EVM opcode

    4. Arrays:跟数组相关的库合约

18.import

import用法

  • 通过源文件相对位置导入,例子:

    1
    2
    3
    4
    5
    6
    文件结构
    ├── Import.sol
    └── Yeye.sol

    // 通过文件相对位置import
    import './Yeye.sol';
  • 通过源文件网址导入网上的合约的全局符号,例子:

    1
    2
    // 通过网址引用
    import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
  • 通过npm的目录导入,例子:

    1
    import '@openzeppelin/contracts/access/Ownable.sol';
  • 通过指定全局符号导入合约特定的全局符号,例子:

    1
    import {Yeye} from './Yeye.sol';
  • 引用(import)在代码中的位置为:在声明版本号之后,在其余代码之前。