0%

StudyRecord-Solidity异常处理

Pre:

require和assert的gas消耗对比问题,学习记录

写智能合约经常会出bugsolidity中的异常命令帮助我们debug,也可以提示用户发生了什么错误。

有4个关键词:

  • error

  • require

  • assert

  • revert

error:

errorsolidity 0.8版本新加的内容,方便且高效(省gas)地向用户解释操作失败的原因。人们可以在contract之外定义异常。(模块化的前提,代码更易读)

使用方法:

下面,我们定义一个TransferNotOwner异常,当用户不是代币owner的时候尝试转账,会抛出错误:

1
2
3
4
5
6
7
8
9
error TransferNotOwner(); // 自定义error

// 在执行当中,error必须搭配revert(回退)命令使用。
function transferOwner1(uint256 tokenId, address newOwner) public {
if(_owners[tokenId] != msg.sender){
revert TransferNotOwner();
}
_owners[tokenId] = newOwner;
}

require:

require is used to validate inputs and conditions before execution
用于在代码执行之前验证输入和条件,常见3种使用场景:

  • 外部input的参数

  • 在执行某段逻辑前,判断条件(条件很多,要通过测试不断补充)

  • 其他函数的返回值

require命令是solidity 0.8版本之前抛出异常的常用方法,目前很多主流合约仍然还在使用它。

缺点就是gas随着描述异常的字符串长度增加,比error命令要高。

使用方法:

require(检查条件,"异常的描述"),当检查条件不成立的时候,就会抛出异常。

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

Uni例子:

看看成熟代码里怎么用

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
/// @inheritdoc IUniswapV3Factory
function createPool(
address tokenA,
address tokenB,
uint24 fee
) external override noDelegateCall returns (address pool) {
require(tokenA != tokenB);
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0));
int24 tickSpacing = feeAmountTickSpacing[fee];
require(tickSpacing != 0);
require(getPool[token0][token1][fee] == address(0));
pool = deploy(address(this), token0, token1, fee, tickSpacing);
getPool[token0][token1][fee] = pool;
// populate mapping in the reverse direction, deliberate choice to avoid the cost of comparing addresses
getPool[token1][token0][fee] = pool;
emit PoolCreated(token0, token1, fee, tickSpacing, pool);
}

/// @inheritdoc IUniswapV3Factory
function setOwner(address _owner) external override {
require(msg.sender == owner);
emit OwnerChanged(owner, _owner);
owner = _owner;
}

revert:

revert 类似 require,相比可选填写异常描述的require,准确的描述会更user friendly,但遇到复杂的检验条件时,会选用revert.

assert:

assert is used to check for code that should never be false.
Failing assertion probably means that there is a bug.

断言真假,常用于after execution的位置

使用场景:一般用于用于内部错误检查或去检查一些不变量,因为它不能解释抛出异常的原因,所以不是对外使用的,是在内部使用的。

使用方法:

它的用法很简单,assert(检查条件),当检查条件不成立的时候,就会抛出异常。

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

Uni例子:

看看成熟代码里怎么用

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
function check_liquidityNet_invariant() internal {
int128 liquidityNet = 0;
for (uint256 i = 0; i < usedTicks.length; i++) {
(, int128 tickLiquidityNet, , ) = pool.ticks(usedTicks[i]);
int128 result = liquidityNet + tickLiquidityNet;
assert(
(tickLiquidityNet >= 0 && result >= liquidityNet) || (tickLiquidityNet < 0 && result < liquidityNet)
);
liquidityNet = result;
}

// prop #20 常用于尾部,执行逻辑后
assert(liquidityNet == 0);
}

function check_liquidity_invariant() internal {
(, int24 currentTick, , , , , ) = pool.slot0();

int128 liquidity = 0;
for (uint256 i = 0; i < usedTicks.length; i++) {
int24 tick = usedTicks[i];

if (tick <= currentTick) {
(, int128 tickLiquidityNet, , ) = pool.ticks(tick);

int128 result = liquidity + tickLiquidityNet;
assert((tickLiquidityNet >= 0 && result >= liquidity) || (tickLiquidityNet < 0 && result < liquidity));
liquidity = result;
}
}

// prop #21
assert(uint128(liquidity) == pool.liquidity());
assert(liquidity >= 0);
}

gas对比:

基于代码,简单对比,好像没啥意义。。。😨😨😨
仅仅是小小gas优化的,单个交易看不出啥,multi tx才能看出来。

Keyword gas
require 24440
assert 24446
error 24457
require带异常描述字符串 24743

Question

Q1: revert返还gas吗?

理论上require的报错会将余下的gas返回给user,而assert会全部没收,而15_Error中实验结果,是assert比require更少的损耗,问题出在哪里? —— issue

群友回复:

0.8版本之前 assert()会消耗完所有gas

20220714140615

0.8版本后assert()在编译时将操作码从FE改为了FD不会消耗完gas

20220714140629

Solidity v0.8.0 重大更新

Failing assertions and other internal checks like division by zero or arithmetic overflow do not use the invalid opcode but instead the revert opcode

This will save gas on errors

assert的操作码改变了:invalid opcode ->revert opcode,会返还gas

Q2: require带错误描述为啥更耗gas?

ErrorRequire.evm Vs ErrorRequireText.evm

20220714142719

所有命运赠送的礼物,早已在暗中标好了价格 —— 茨威格

每个opcode都有其gas price,多了字符串要存储,就多了opcode,就多了gas

Summary:

Keyword 特点 使用场景
require 外部输入,before execution 1.外部input的参数 2.在执行某段逻辑前判断条件 3.其他函数的返回值
error 升级版require,错误可模块化
revert 偷懒版require不写异常描述,复杂的检验条件 复杂的检验条件
assert 断言真假,after execution 常用于func尾部
1
2
3
4
5
6
7
8
9
10
11
12
function (input):
|
|
require/升级用error/偷懒用revert
|
| before execution
|
| 某个函数() / 某段执行逻辑
|
| after execution
|
assert 断言某些输出真假
  • 输入用require

  • 升级用Error

  • 偷懒用revert

  • 输出用assert 😋😋😋

opcode大法好: 0.8.0版本前后,assert的操作码改变了:invalid opcode ->revert opcode,会返还gas

给自己的建议:

  • 多看英文资料,中文资料有毒,思路好乱 🤪🤪🤪

  • 学语法就学语法,要注重使用场景,多写多使用,gas优化这类进阶知识,要学了evm、opcode先

  • 多积累基础知识,少超前诠释知识

20220714144530

  • 除了细读文档,还要看看版本更新了啥新特性,太难了

  • 新版本出的新特性优先使用,既省gas,又可以将错误模块化,不用满天星require语句。双倍快乐😁😁😁

  • change view step by step,think step ↓

  • evm、opcode大法好 🤙

Refs: