区块链学习第九期

myBlog

solidity小细节【下篇】

[toc] ---

9:error、require、assert

这三种都是抛出异常的方法 但是用法和消耗的 gas 不一样

error 方法 gas 最少,其次是 assert,require 方法消耗 gas 最多!因此,error 既可以告知用户抛出异常的原因,又能省 gas

Error

error 是 solidity 0.8 版本新加的内容,方便且高效(省 gas)地向用户解释操作失败的原因。人们可以在 contract 之外定义异常。下面,我们定义一个 TransferNotOwner 异常,当用户不是代币 owner 的时候尝试转账,会抛出错误:

1
error TransferNotOwner(); // 自定义error

在执行当中,error 必须搭配 revert(回退)命令使用

1
2
3
4
5
6
function transferOwner1(uint256 tokenId, address newOwner) public {
if(_owners[tokenId] != msg.sender){
revert TransferNotOwner();
}
_owners[tokenId] = newOwner;
}

定义了一个 transferOwner1()函数,它会检查代币的 owner 是不是发起人,如果不是,就会抛出 TransferNotOwner 异常;如果是的话,就会转账。

Require(平常用得很多)

require 命令是 solidity 0.8 版本之前抛出异常的常用方法,目前很多主流合约仍然还在使用它。它很好用,唯一的缺点就是 gas 随着描述异常的字符串长度增加,比 error 命令要高。使用方法:require(检查条件,”异常的描述”),当检查条件不成立的时候,就会抛出异常。

用 require 命令重写一下上面的 transferOwner 函数:

1
2
3
4
function transferOwner2(uint256 tokenId, address newOwner) public {
require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
_owners[tokenId] = newOwner;
}

Assert

assert 命令一般用于程序员写程序 debug,因为它不能解释抛出异常的原因(比 require 少个字符串)。它的用法很简单,assert(检查条件),当检查条件不成立的时候,就会抛出异常。

用 assert 命令重写一下上面的 transferOwner 函数:

1
2
3
4
function transferOwner3(uint256 tokenId, address newOwner) public {
assert(_owners[tokenId] == msg.sender);
_owners[tokenId] = newOwner;
}

10:库合约(不同点)

为了提升 solidity 代码的复用性和减少 gas 而存在(就像查字典一样)

特点

  • 不能存在状态变量
  • 不能够继承或被继承
  • 不能接收以太币
  • 不可以被销毁

如何在合约中使用

  1. 利用 using for 指令
1
2
3
4
5
6
7
// 利用using for指令
using Strings for uint256;
function getString1(uint256 _number) public pure returns(string memory){
// 库函数会自动添加为uint256型变量的成员
return _number.toHexString();
//这里因为_number是uint256 所以自身就含有了Strings的方法
}
  1. 通过库合约名称调用库函数
1
2
3
4
// 直接通过库合约名调用
function getString2(uint256 _number) public pure returns(string memory){
return Strings.toHexString(_number);
}

常用库函数

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

11:接收 ETH:receive、fallback

receive

receive()只用于处理接收 ETH。一个合约最多有一个 receive()函数,声明方式与一般函数不一样,不需要 function 关键字:receive() external payable { … }。receive()函数不能有任何的参数,不能返回任何值,必须包含 external 和 payable。

fallback

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


12:发送 ETH:transfer、send、call

transfer

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

  • transfer()的 gas 限制是 2300,足够用于转账,但对方合约的
  • fallback()或 receive()函数不能实现太复杂的逻辑。
  • transfer()如果转账失败,会自动 revert(回滚交易)

send

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

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

call

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

  • call()没有 gas 限制,可以支持对方合约 fallback()或 receive()函数实现复杂逻辑。
  • call()如果转账失败,不会 revert。
  • call()的返回值是(bool, data),其中 bool 代表着转账成功或失败,需要额外代码处理一下。
    代码样例
1
2
3
4
5
6
7
8
// call()发送ETH
function callETH(address payable _to, uint256 amount) external payable{
// 处理下call的返回值,如果失败,revert交易并发送error
(bool success,) = _to.call{value: amount}("");
if(!success){
revert CallFailed();
}
}

区别

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

13:call 和 delegatecall

最根本的区别就是:
A 调用call改变的是 B 的变量
A 调用delegatecall改变的是 A 的变量

应用场景:当我部署 A 后,代码已经固定不能改变,我只有+1 的方法没有+2 的方法怎么办? 在 B 中实现+2 的方法 在 A 中用 delegatecall 调用即可,这样数据就不会存储在另一个地方

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
contract A{
uint public num;

function setNum(_num) public{
num = _num+1;
}

//1. 引用合约类型直接调用里面的函数 B里面的num改变 A不变
function bsetNum(address _baddress,uint _num) public{
B b = B(_baddress);
b.setNum(_num);
}


//2. B里面的num改变 A不变
function bsetNumCall(address _baddress,uint _num) public{
(bool res,) = _bAddress.call(abi.encodeWithSignatrue("setNum(uint256)",_num));
if(!res) revert();
}


//2. A里面的num改变 B不变
function bsetNumDelegateCall(address _baddress,uint _num) public{
(bool res,) = _bAddress.delegatecall(abi.encodeWithSignatrue("setNum(uint256)",_num));
if(!res) revert();
}

}

contract B{
uint publib num;
function setNum(_num) public{
num = _num+2;
}

}


14:create2 实际应用场景


15:selfdestruct

selfdestruct

selfdestruct 命令可以用来删除智能合约,并将该合约剩余 ETH 转到指定地址。selfdestruct 是为了应对合约出错的极端情况而设计的。它最早被命名为 suicide(自杀),但是这个词太敏感。为了保护抑郁的程序员,改名为 selfdestruct。

如何使用 selfdestruct

selfdestruct(_addr);
其中_addr 是接收合约中剩余 ETH 的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract DeleteContract {

uint public value = 10;

constructor() payable {}

receive() external payable {}

function deleteContract() external {
// 调用selfdestruct销毁合约,并把剩余的ETH转给msg.sender
selfdestruct(payable(msg.sender));
}

function getBalance() external view returns(uint balance){
balance = address(this).balance;
}
}

16:ABI、Hash、selector


17:Solidity 应用