Solidity入门学习(三)
2025-02-05 23:39:27 # Web3

重载

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

测试代码Overloading.sol,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract Overloading{
function saySomething() public pure returns(string memory){
return ("Nothing");
}
function saySomething(string memory something) public pure returns (string memory) {
return(something);
}
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}
function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}
}

函数重载

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

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

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

Overloading.sol 合约为例,在 Remix 上编译部署后,分别调用重载函数 saySomething()saySomething(string memory something),可以看到他们返回了不同的结果,被区分为不同的函数。

image-20250204032523893

参数匹配(Argument Matching)

在调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。 如果出现多个匹配的重载函数,则会报错。下面这个例子有两个叫f()的函数,一个参数为uint8,另一个为uint256

1
2
3
4
5
6
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}
function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}

我们调用f(50),因为50既可以被转换为uint8,也可以被转换为uint256,因此会报错。

库合约

这一讲,我们用ERC721的引用的库合约Strings为例介绍Solidity中的库合约(Library),并总结了常用的库合约。

完整的测试代码Library.sol如下:

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.21;

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);
}
}


// 用函数调用另一个库合约
contract UseLibrary{
// 利用using for操作使用库
using Strings for uint256;
function getString1(uint256 _number) public pure returns(string memory){
// 库函数会自动添加为uint256型变量的成员
return _number.toHexString();
}

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

库合约

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

他和普通合约主要有以下几点不同:

  1. 不能存在状态变量
  2. 不能够继承或被继承
  3. 不能接收以太币
  4. 不可以被销毁

需要注意的是,库合约中的函数可见性如果被设置为public或者external,则在调用函数时会触发一次delegatecall。而如果被设置为internal,则不会引起。对于设置为private可见性的函数来说,其仅能在库合约中可见,在其他合约中不可用。

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转换为10进制的stringtoHexString()uint256转换为16进制的string

如何使用库合约?

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

  • 利用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();
}
  • 通过库合约名称调用函数
1
2
3
4
// 直接通过库合约名调用
function getString2(uint256 _number) public pure returns(string memory){
return Strings.toHexString(_number);
}

我们部署合约并输入130测试一下,两种方法均能返回正确的16进制string 0x82。证明我们调用库合约成功!

image-20250204035623180

常见的库合约有:

  1. Strings:将uint256转换为String
  2. Address:判断某个地址是否为合约地址
  3. Create2:更安全的使用Create2 EVM opcode
  4. Arrays:跟数组相关的库合约

Import

在Solidity中,import语句可以帮助我们在一个文件中引用另一个文件的内容,提高代码的可重用性和组织性。本教程将向你介绍如何在Solidity中使用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)在代码中的位置为:在声明版本号之后,在其余代码之前。

测试导入结果

我们可以用下面这段代码测试是否成功导入了外部源代码:

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

// 通过文件相对位置import
import './Yeye.sol';
// 通过`全局符号`导入特定的合约
import {Yeye} from './Yeye.sol';
// 通过网址引用
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
// 引用OpenZeppelin合约
import '@openzeppelin/contracts/access/Ownable.sol';

contract Import {
// 成功导入Address库
using Address for address;
// 声明yeye变量
Yeye yeye = new Yeye();

// 测试是否能调用yeye的函数
function test() external{
yeye.hip();
}
}

Yeye.sol代码如下:

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

// 第10讲合约继承中的Yeye合约
contract Yeye {
event Log(string msg);

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

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

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

测试结果如下:

image-20250204041504259

接收和发送ETH

本讲主要介绍ETH的接收和发送。

接收ETH

Solidity支持两种特殊的回调函数,receive()fallback(),他们主要在两种情况下被使用:

  • 接收ETH
  • 处理合约中不存在的函数调用(代理合约proxy contract)

注意⚠️:在Solidity 0.6.x版本之前,语法上只有 fallback() 函数,用来接收用户发送的ETH时调用以及在被调用函数签名没有匹配到时,来调用。 0.6版本之后,Solidit才将 fallback() 函数拆分成 receive()fallback() 两个函数。

接收ETH函数receive

receive()函数是在合约收到ETH转账时被调用的函数。

一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }receive()函数不能有任何的参数,不能返回任何值,必须包含externalpayable

当合约接收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);
}

有些恶意合约,会在receive() 函数(老版本的话,就是 fallback() 函数)嵌入恶意消耗gas的内容或者使得执行故意失败的代码,导致一些包含退款和转账逻辑的合约不能正常工作,因此写包含退款等逻辑的合约时候,一定要注意这种情况。

回退ETH函数fallback

fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract

fallback()声明时不需要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);
}

receivefallback的区别

receivefallback都能够用于接收ETH,他们触发的规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
触发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)。

测试代码演示

完整的测试代码Fallback.sol,如下:

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.21;

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

// 定义事件
event receivedCalled(address Sender, uint Value);
event fallbackCalled(address Sender, uint Value, bytes Data);

// 接收ETH时释放Received事件
receive() external payable {
emit receivedCalled(msg.sender, msg.value);
}

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

部署并执行代码,VALUE栏中输入要发送到合约中的金额,CALLDATA留空,点击Transact,成功触发到receivedCalled事件。

image-20250204160942747

同样,VALUE栏中输入要发送到合约中的金额,CALLDATA输入任意编写的0xabcd,点击Transact,成功触发到fallbackCalled事件。

image-20250204161621064

发送ETH

Solidity有三种方法向其他合约发送ETH,他们是:transfer()send()call(),其中call()是被鼓励的用法。

完整的测试代码SendETH.sol如下:

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.21;

// 3种方法发送ETH
// transfer: 2300 gas, revert
// send: 2300 gas, return bool
// call: all gas, return (bool, data)

error SendFailed(); // 用send发送ETH失败error
error CallFailed(); // 用call发送ETH失败error

contract SendETH {
// 构造函数,payable使得部署的时候可以转eth进去
constructor() payable{}
// receive方法,接收eth时被触发
receive() external payable{}

// 用transfer()发送ETH
function transferETH(address payable _to, uint256 amount) external payable{
_to.transfer(amount);
}

// send()发送ETH
function sendETH(address payable _to, uint256 amount) external payable{
// 处理下send的返回值,如果失败,revert交易并发送error
bool success = _to.send(amount);
if(!success){
revert SendFailed();
}
}

// call()发送ETH
function callETH(address payable _to, uint256 amount) external payable{
// 处理下call的返回值,如果失败,revert交易并发送error
(bool success,) = _to.call{value: amount}("");
if(!success){
revert CallFailed();
}
}
}

contract ReceiveETH {
// 收到eth事件,记录amount和gas
event Log(uint amount, uint gas);

// receive方法,接收eth时被触发
receive() external payable{
emit Log(msg.value, gasleft());
}

// 返回合约ETH余额
function getBalance() view public returns(uint) {
return address(this).balance;
}
}

接收ETH合约

我们先部署一个接收ETH合约ReceiveETHReceiveETH合约里有一个事件Log,记录收到的ETH数量和gas剩余。还有两个函数,一个是receive()函数,收到ETH被触发,并发送Log事件;另一个是查询合约ETH余额的getBalance()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract ReceiveETH {
// 收到eth事件,记录amount和gas
event Log(uint amount, uint gas);

// receive方法,接收eth时被触发
receive() external payable{
emit Log(msg.value, gasleft());
}

// 返回合约ETH余额
function getBalance() view public returns(uint) {
return address(this).balance;
}
}

部署ReceiveETH合约后,运行getBalance()函数,可以看到当前合约的ETH余额为0

image-20250204164248838

发送ETH合约

我们将实现三种方法向ReceiveETH合约发送ETH。首先,先在发送ETH合约SendETH中实现payable构造函数receive(),让我们能够在部署时和部署后向合约转账。

1
2
3
4
5
6
contract SendETH {
// 构造函数,payable使得部署的时候可以转eth进去
constructor() payable{}
// receive方法,接收eth时被触发
receive() external payable{}
}

Transfer用法(次优)

  • 用法是接收方地址.transfer(发送ETH数额)

  • transfer()gas限制是2300,足够用于转账,但对方合约的fallback()receive()函数不能实现太复杂的逻辑。

  • transfer()如果转账失败,会自动revert(回滚交易)。

代码样例,注意里面的_toReceiveETH合约的地址amountETH转账金额:

1
2
3
4
// 用transfer()发送ETH
function transferETH(address payable _to, uint256 amount) external payable{
_to.transfer(amount);
}

部署SendETH合约后,对ReceiveETH合约发送ETH。

image-20250204172302122

ReceiveETH合约成功接收到转账的ETH。

image-20250204172327987

amount>value,转账失败,发生revert

image-20250204172500152

Send用法

用法是接收方地址.send(发送ETH数额)

  • send()gas限制是2300,足够用于转账,但对方合约的fallback()receive()函数不能实现太复杂的逻辑。
  • send()如果转账失败,不会revert
  • send()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下。

代码样例:

1
2
3
4
5
6
7
8
9
10
error SendFailed(); // 用send发送ETH失败error

// send()发送ETH
function sendETH(address payable _to, uint256 amount) external payable{
// 处理下send的返回值,如果失败,revert交易并发送error
bool success = _to.send(amount);
if(!success){
revert SendFailed();
}
}

amount>value,转账失败,发生revert

image-20250204172829438

Call用法(提倡使用)

用法是接收方地址.call{value: 发送ETH数额}("")

  • call()没有gas限制,可以支持对方合约fallback()receive()函数实现复杂逻辑。
  • call()如果转账失败,不会revert
  • call()的返回值是(bool, bytes),其中bool代表着转账成功或失败,需要额外代码处理一下。

代码样例:

1
2
3
4
5
6
7
8
9
10
error CallFailed(); // 用call发送ETH失败error

// call()发送ETH
function callETH(address payable _to, uint256 amount) external payable{
// 处理下call的返回值,如果失败,revert交易并发送error
(bool success,) = _to.call{value: amount}("");
if(!success){
revert CallFailed();
}
}

部署SendETH合约后,对ReceiveETH合约发送ETH。

image-20250204165726035

ReceiveETH合约成功接收到转账的ETH。

image-20250204165840188

amount>value,转账失败,发生revert

image-20250204170920553

总结

这一讲,我们介绍Solidity三种发送ETH的方法:transfersendcall

  • **call没有gas限制,最为灵活,是最提倡的方法!!!**⚠️⚠️⚠️
  • transfer2300 gas限制,但是发送失败会自动revert交易,是次优选择;
  • send2300 gas限制,而且发送失败不会自动revert交易,几乎没有人用它。

参考

https://www.wtf.academy/zh/course/solidity102/Fallback

https://www.wtf.academy/zh/course/solidity102/SendETH