Solidity 0.6 终于可以捕获错误啦
当EVM中的交易执行触发[revert](https://eips.ethereum.org/EIPS/eip-140)时,所有状态更改都将回滚并中止执行。 因此,来自现代编程语言的开发人员在编写Solidity时,都可能最终会搜索“如何在Solidity中try/catch”来处理这些回滚。
Solidity 0.6的[新功能](https://learnblockchain.cn/docs/solidity/control-structures.html#try-catch)里最酷的功能之一是使用`try/catch`语句。
> [try/catch 的文档可以点击这里查看](https://learnblockchain.cn/docs/solidity/control-structures.html#try-catch)
### 为什么需要 try / catch
`try/catch` 结构可以用于许多场景:
– 如果一个调用回滚(revert)了,我们不想终止交易的执行。
– 我们想在同一个交易中重试调用、存储错误状态、对失败的调用做出处理等等。
在 Solidity 0.6 之前,模拟 `try/catch` 仅有的方式是使用低级的调用如: `call`, `delegatecall` 和 `staticcall`.
这是一个简单的示例,说明如何内部调用同一合约的另一个函数中实现某种`try/catch`:
“`javascript
pragma solidity <0.6.0;
contract OldTryCatch {
function execute(uint256 amount) external {
// 如果执行失败,低级的call会返回 `false`
(bool success, bytes memory returnData) = address(this).call(
abi.encodeWithSignature(
“onlyEven(uint256)”,
amount
)
);
if (success) {
// handle success
} else {
// handle exception
}
}
function onlyEven(uint256 a) public {
// Code that can revert
require(a % 2 == 0, “Ups! Reverting”);
// …
}
}
“`
当调用 `execute(uint256 amount)`, 输入的参数 `amount` 会通过低级的call调用传给 `onlyEven(uint256)` 函数, call 调用会返回 `bool` 值作为第一个参数来指示调用的成功与否,而不会让整个交易失败。
> 参考文档: [Solidity中文 文档-地址类型的成员](https://learnblockchain.cn/docs/solidity/types.html#members-of-addresses)
请注意,在对 `onlyEven(uint256)` 的低级call调用返回了`false`的情况下,它会(必需)还原在低级调用执行中所做的状态更改,但是在调用之前和/或之后的更改不会被还原应用。
这种 `try/catch`的定制实现虽然有点脆弱,但它既可以用于从同一合约(如刚刚的例子)中调用函数,也可以(更多的)用于外部合约的调用。
这是控制外部调用的错误的一种有用方法,但我们应始终记住,由于执行我们不能信任的外部代码可能会出现安全性问题,因此**不建议使用低级调用**。
这是为什么 `try/catch` 特性用于**外部调用**引入 ,在最新的编译器中,可以这样写:
“`javascript
pragma solidity <0.7.0;
contract CalledContract {
function someFunction() external {
// Code that reverts
revert();
}
}
contract TryCatcher {
event CatchEvent();
event SuccessEvent();
CalledContract public externalContract;
constructor() public {
externalContract = new CalledContract();
}
function execute() external {
try externalContract.someFunction() {
// Do something if the call succeeds
emit SuccessEvent();
} catch {
// Do something in any other case
emit CatchEvent();
}
}
}
“`
### try/catch 概述
就像前面提到的,新功能 try/catch 仅适用于**外部调用**。
> 参考文档: [Solidity中文 文档-外部调用](https://learnblockchain.cn/docs/solidity/control-structures.html#external-function-calls)
如果我们想在合同中的内部调用中使用`try/catch`模式(如第一个示例),我们仍然可以使用前面介绍的低级调用的方法,或者可以使用全局变量`this`来调用内部函数,就像外部调用一样。
如上面的例子:
“`
try this.onlyEven(3) {
…
} catch {
…
}
“`
如果尝试使用新语法进行低级调用,则编译器将提示错误:

每当我们尝试使用`try/catch`语法进行低级调用时,编译器都会返回TypeError错误提示。
如果仔细阅读了编译器错误信息,则TypeError提示会指出,`try/catch`甚至可以用于创建合约,让我们来尝试一下:
“`javascript
pragma solidity <0.7.0;
contract CalledContract {
constructor() public {
// Code that reverts
revert();
}
// …
}
contract TryCatcher {
// …
function execute() public {
try new CalledContract() {
emit SuccessEvent();
} catch {
emit CatchEvent();
}
}
}
“`
要注意,在`try`代码块内的任何内容仍然可以停止执行, `try`仅针对于call。 例如,在`try` 成功后,依旧可以 revert 交易,例如下面的例子:
“`javascript
function execute() public {
try externalContract.someFunction() {
// 尽管外部调用成功了, 依旧可以回退交易。
revert();
} catch {
…
}
“`
因此,请注意:**`try` 代码块内的 `revert` 不会被`catch`本身捕获**。
### 返回值和作用域内变量
Try / catch 允许使用从外部调用返回值和作用域内变量。
构造调用的例子:
“`javascript
contract TryCatcher {
// …
function execute() public {
try new CalledContract() returns(CalledContract returnedInstance) {
// returnedInstance 是新部署合约的地址
emit SuccessEvent();
} catch {
emit CatchEvent();
}
}
}
“`
外部调用:
“`javascript
contract CalledContract {
function getTwo() public returns (uint256) {
return 2;
}
}
contract TryCatcher {
CalledContract public externalContract;
// …
function execute() public returns (uint256, bool) {
try externalContract.getTwo() returns (uint256 v) {
uint256 newValue = v + 2;
return (newValue, true);
} catch {
emit CatchEvent();
}
// …
}
}
“`
注意本地变量`newValue` 和返回值只在 `try` 代码块内有效。这同样适用于在`catch`块内声明的任何变量。
要在`catch`语句中使用返回值,我们可以使用以下语法:
“`javascript
contract TryCatcher {
event ReturnDataEvent(bytes someData);
// …
function execute() public returns (uint256, bool) {
try externalContract.someFunction() {
// …
} catch (bytes memory returnData) {
emit ReturnDataEvent(returnData);
}
}
}
“`
外部调用返回的数据将转换为`bytes` ,并可以在`catch` 块内进行访问。 注意,该`catch` 中考虑了各种可能的 revert 原因,并且如果由于某种原因解码返回数据失败,则将在调用合约的上下文中产生该失败-因此执行`try/catch`的交易也会失败。
### 指定 catch 条件子句
Solidity 的 `try/catch`也可以包括特定的catch条件子句。 已经可以使用的第一个特定的`catch`条件子句是:
“`javascript
contract TryCatcher {
event ReturnDataEvent(bytes someData);
event CatchStringEvent(string someString);
event SuccessEvent();
// …
function execute() public {
try externalContract.someFunction() {
emit SuccessEvent();
} catch Error(string memory revertReason) {
emit CatchStringEvent(revertReason);
} catch (bytes memory returnData) {
emit ReturnDataEvent(returnData);
}
}
}
“`
在这里,如果还原是由`require(condition,”reason string”)`或`revert(“reason string”)`引起的,则错误签名与`catch Error(string memory revertReason)`子句匹配,然后与之匹配块被执行。 在任何其他情况下,(例如, `assert`失败)都会执行更通用的 `catch (bytes memory returnData)` 子句。
注意,`catch Error(string memory revertReason)`不能捕获除上述两种情况以外的任何错误。 如果我们仅使用它(不使用其他子句),最终将丢失一些错误。 通常,必须将`catch`或`catch(bytes memory returnData)`与`catch Error(string memory revertReason)`一起使用,以确保我们涵盖了所有可能的revert原因。
在一些特定的情况下,如果`catch Error(string memory revertReason)` 解码返回的字符串失败,`catch(bytes memory returnData)`(如果存在)将能够捕获它。
计划在将来的Solidity版本中使用更多条件的`catch`子句。
### Gas 失败
如果交易没有足够的gas执行,则`out of gas error` 是不能捕获到的。
在某些情况下,我们可能需要为外部调用指定gas,因此,即使交易中有足够的gas,如果外部调用的执行需要的gas比我们设置的多,内部`out of gas` 错误可能会被低级的`catch`子句捕获。
“`javascript
pragma solidity <0.7.0;
contract CalledContract {
function someFunction() public returns (uint256) {
require(true, “This time not reverting”);
}
}
contract TryCatcher {
event ReturnDataEvent(bytes someData);
event SuccessEvent();
CalledContract public externalContract;
constructor() public {
externalContract = new CalledContract();
}
function execute() public {
// Setting gas to 20
try externalContract.someFunction.gas(20)() {
// …
} catch Error(string memory revertReason) {
// …
} catch (bytes memory returnData) {
emit ReturnDataEvent(returnData);
}
}
}
“`
当gas设置为20时,`try`调用的执行将用掉所有的 gas,最后一个catch语句将捕获异常:`catch (bytes memory returnData)`。 相反,将gas设置为更大的量(例如:2000)将执行`try`块会成功。
### 结论
总结一下,这里是使用Solidity新添加的`try/catch`时要记住的事项:
– 这是**仅仅提供给外部调用**的特性,如上所述。部署新合约也被视为外部调用。
– 该功能能够捕获仅在调用内部产生的异常。调用后的 `try` 代码块是在成功之后执行。不会捕获try 代码块中的任何异常。
– 如果函数调用返回一些变量,则可以在以下执行块中使用它们(如以上示例中所述)。
– 如果执行了 `try` 成功代码块,则必须声明与函数调用实际返回值相同类型的变量。
– 如果执行了低级的`catch`块,则返回值是类型为`bytes`的变量。任何特定条件的`catch`子句都有其自己的返回值类型。
– 请记住,低级`catch (bytes memory returnData)` 子句能够捕获所有异常,而特定条件的`catch`子句只捕获对应的错误。处理各种异常时,请考虑同时使用两者。
– 在为 `try` 外部调用设置特定的gas使用量时,低级的`catch`子句会捕获最终的`out of gas`错误。 但如果交易本身没有足够的 gas执行代码,则`out of gas`是没法捕获的。
本文翻译自 openzeppelin 论坛,[原文](https://forum.openzeppelin.com/t/a-brief-analysis-of-the-new-try-catch-functionality-in-solidity-0-6/2564)
在以太坊中对智能合约进行编程与常规开发人员所用的编程有很大不同,并且缺乏基本处理错误工具一直是一个问题,经常导致智能合约逻辑“破裂”。
当EVM中的交易执行触发revert时,所有状态更改都将回滚并中止执行。 因此,来自现代编程语言的开发人员在编写Solidity时,都可能最终会搜索“如何在Solidity中try/catch”来处理这些回滚。
Solidity 0.6的新功能里最酷的功能之一是使用try/catch
语句。
try/catch 的文档可以点击这里查看
为什么需要 try / catch
try/catch
结构可以用于许多场景:
-
如果一个调用回滚(revert)了,我们不想终止交易的执行。
-
我们想在同一个交易中重试调用、存储错误状态、对失败的调用做出处理等等。
在 Solidity 0.6 之前,模拟 try/catch
仅有的方式是使用低级的调用如: call
, delegatecall
和 staticcall
.
这是一个简单的示例,说明如何内部调用同一合约的另一个函数中实现某种try/catch
:
pragma solidity <0.6.0;
contract OldTryCatch {
function execute(uint256 amount) external {
// 如果执行失败,低级的call会返回 `false`
(bool success, bytes memory returnData) = address(this).call(
abi.encodeWithSignature(
"onlyEven(uint256)",
amount
)
);
if (success) {
// handle success
} else {
// handle exception
}
}
function onlyEven(uint256 a) public {
// Code that can revert
require(a % 2 == 0, "Ups! Reverting");
// ...
}
}
当调用 execute(uint256 amount)
, 输入的参数 amount
会通过低级的call调用传给 onlyEven(uint256)
函数, call 调用会返回 bool
值作为第一个参数来指示调用的成功与否,而不会让整个交易失败。
参考文档: Solidity中文 文档-地址类型的成员
请注意,在对 onlyEven(uint256)
的低级call调用返回了false
的情况下,它会(必需)还原在低级调用执行中所做的状态更改,但是在调用之前和/或之后的更改不会被还原应用。
这种 try/catch
的定制实现虽然有点脆弱,但它既可以用于从同一合约(如刚刚的例子)中调用函数,也可以(更多的)用于外部合约的调用。
这是控制外部调用的错误的一种有用方法,但我们应始终记住,由于执行我们不能信任的外部代码可能会出现安全性问题,因此不建议使用低级调用。
这是为什么 try/catch
特性用于外部调用引入 ,在最新的编译器中,可以这样写:
pragma solidity <0.7.0;
contract CalledContract {
function someFunction() external {
// Code that reverts
revert();
}
}
contract TryCatcher {
event CatchEvent();
event SuccessEvent();
CalledContract public externalContract;
constructor() public {
externalContract = new CalledContract();
}
function execute() external {
try externalContract.someFunction() {
// Do something if the call succeeds
emit SuccessEvent();
} catch {
// Do something in any other case
emit CatchEvent();
}
}
}
try/catch 概述
就像前面提到的,新功能 try/catch 仅适用于外部调用。
参考文档: Solidity中文 文档-外部调用
如果我们想在合同中的内部调用中使用try/catch
模式(如第一个示例),我们仍然可以使用前面介绍的低级调用的方法,或者可以使用全局变量this
来调用内部函数,就像外部调用一样。
如上面的例子:
try this.onlyEven(3) {
...
} catch {
...
}
如果尝试使用新语法进行低级调用,则编译器将提示错误:
每当我们尝试使用try/catch
语法进行低级调用时,编译器都会返回TypeError错误提示。
如果仔细阅读了编译器错误信息,则TypeError提示会指出,try/catch
甚至可以用于创建合约,让我们来尝试一下:
pragma solidity <0.7.0;
contract CalledContract {
constructor() public {
// Code that reverts
revert();
}
// ...
}
contract TryCatcher {
// ...
function execute() public {
try new CalledContract() {
emit SuccessEvent();
} catch {
emit CatchEvent();
}
}
}
要注意,在try
代码块内的任何内容仍然可以停止执行, try
仅针对于call。 例如,在try
成功后,依旧可以 revert 交易,例如下面的例子:
function execute() public {
try externalContract.someFunction() {
// 尽管外部调用成功了, 依旧可以回退交易。
revert();
} catch {
...
}
因此,请注意:try
代码块内的 revert
不会被catch
本身捕获。
返回值和作用域内变量
Try / catch 允许使用从外部调用返回值和作用域内变量。
构造调用的例子:
contract TryCatcher {
// ...
function execute() public {
try new CalledContract() returns(CalledContract returnedInstance) {
// returnedInstance 是新部署合约的地址
emit SuccessEvent();
} catch {
emit CatchEvent();
}
}
}
外部调用:
contract CalledContract {
function getTwo() public returns (uint256) {
return 2;
}
}
contract TryCatcher {
CalledContract public externalContract;
// ...
function execute() public returns (uint256, bool) {
try externalContract.getTwo() returns (uint256 v) {
uint256 newValue = v + 2;
return (newValue, true);
} catch {
emit CatchEvent();
}
// ...
}
}
注意本地变量newValue
和返回值只在 try
代码块内有效。这同样适用于在catch
块内声明的任何变量。
要在catch
语句中使用返回值,我们可以使用以下语法:
contract TryCatcher {
event ReturnDataEvent(bytes someData);
// ...
function execute() public returns (uint256, bool) {
try externalContract.someFunction() {
// ...
} catch (bytes memory returnData) {
emit ReturnDataEvent(returnData);
}
}
}
外部调用返回的数据将转换为bytes
,并可以在catch
块内进行访问。 注意,该catch
中考虑了各种可能的 revert 原因,并且如果由于某种原因解码返回数据失败,则将在调用合约的上下文中产生该失败-因此执行try/catch
的交易也会失败。
指定 catch 条件子句
Solidity 的 try/catch
也可以包括特定的catch条件子句。 已经可以使用的第一个特定的catch
条件子句是:
contract TryCatcher {
event ReturnDataEvent(bytes someData);
event CatchStringEvent(string someString);
event SuccessEvent();
// ...
function execute() public {
try externalContract.someFunction() {
emit SuccessEvent();
} catch Error(string memory revertReason) {
emit CatchStringEvent(revertReason);
} catch (bytes memory returnData) {
emit ReturnDataEvent(returnData);
}
}
}
在这里,如果还原是由require(condition,"reason string")
或revert("reason string")
引起的,则错误签名与catch Error(string memory revertReason)
子句匹配,然后与之匹配块被执行。 在任何其他情况下,(例如, assert
失败)都会执行更通用的 catch (bytes memory returnData)
子句。
注意,catch Error(string memory revertReason)
不能捕获除上述两种情况以外的任何错误。 如果我们仅使用它(不使用其他子句),最终将丢失一些错误。 通常,必须将catch
或catch(bytes memory returnData)
与catch Error(string memory revertReason)
一起使用,以确保我们涵盖了所有可能的revert原因。
在一些特定的情况下,如果catch Error(string memory revertReason)
解码返回的字符串失败,catch(bytes memory returnData)
(如果存在)将能够捕获它。
计划在将来的Solidity版本中使用更多条件的catch
子句。
Gas 失败
如果交易没有足够的gas执行,则out of gas error
是不能捕获到的。
在某些情况下,我们可能需要为外部调用指定gas,因此,即使交易中有足够的gas,如果外部调用的执行需要的gas比我们设置的多,内部out of gas
错误可能会被低级的catch
子句捕获。
pragma solidity <0.7.0;
contract CalledContract {
function someFunction() public returns (uint256) {
require(true, "This time not reverting");
}
}
contract TryCatcher {
event ReturnDataEvent(bytes someData);
event SuccessEvent();
CalledContract public externalContract;
constructor() public {
externalContract = new CalledContract();
}
function execute() public {
// Setting gas to 20
try externalContract.someFunction.gas(20)() {
// ...
} catch Error(string memory revertReason) {
// ...
} catch (bytes memory returnData) {
emit ReturnDataEvent(returnData);
}
}
}
当gas设置为20时,try
调用的执行将用掉所有的 gas,最后一个catch语句将捕获异常:catch (bytes memory returnData)
。 相反,将gas设置为更大的量(例如:2000)将执行try
块会成功。
结论
总结一下,这里是使用Solidity新添加的try/catch
时要记住的事项:
- 这是仅仅提供给外部调用的特性,如上所述。部署新合约也被视为外部调用。
- 该功能能够捕获仅在调用内部产生的异常。调用后的
try
代码块是在成功之后执行。不会捕获try 代码块中的任何异常。 - 如果函数调用返回一些变量,则可以在以下执行块中使用它们(如以上示例中所述)。
- 如果执行了
try
成功代码块,则必须声明与函数调用实际返回值相同类型的变量。 - 如果执行了低级的
catch
块,则返回值是类型为bytes
的变量。任何特定条件的catch
子句都有其自己的返回值类型。
- 如果执行了
- 请记住,低级
catch (bytes memory returnData)
子句能够捕获所有异常,而特定条件的catch
子句只捕获对应的错误。处理各种异常时,请考虑同时使用两者。 - 在为
try
外部调用设置特定的gas使用量时,低级的catch
子句会捕获最终的out of gas
错误。 但如果交易本身没有足够的 gas执行代码,则out of gas
是没法捕获的。
本文翻译自 openzeppelin 论坛,原文
本文参与区块链开发网写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
- 发表于 2020-04-13 18:24
- 阅读 ( 4697 )
- 学分 ( 156 )
- 分类:以太坊
- 专栏:全面掌握Solidity智能合约开发