Skip to main content

异常

try-catch

try / catch只能捕获来自外部函数调用和合约创建的错误。

try / catch can only catch errors from external function calls and contract creation.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// External contract used for try / catch examples
contract Foo {
address public owner;

constructor(address _owner) {
require(_owner != address(0), "invalid address");
assert(_owner != 0x0000000000000000000000000000000000000001);
owner = _owner;
}

function myFunc(uint x) public pure returns (string memory) {
require(x != 0, "require failed");
return "my func was called";
}
}

contract Bar {
event Log(string message);
event LogBytes(bytes data);

Foo public foo;

constructor() {
// This Foo contract is used for example of try catch with external call
foo = new Foo(msg.sender);
}

// Example of try / catch with external call
// tryCatchExternalCall(0) => Log("external call failed")
// tryCatchExternalCall(1) => Log("my func was called")
function tryCatchExternalCall(uint _i) public {
try foo.myFunc(_i) returns (string memory result) {
emit Log(result);
} catch {
emit Log("external call failed");
}
}

// Example of try / catch with contract creation
// tryCatchNewContract(0x0000000000000000000000000000000000000000) => Log("invalid address")
// tryCatchNewContract(0x0000000000000000000000000000000000000001) => LogBytes("")
// tryCatchNewContract(0x0000000000000000000000000000000000000002) => Log("Foo created")
function tryCatchNewContract(address _owner) public {
try new Foo(_owner) returns (Foo foo) {
// you can use variable foo here
emit Log("Foo created");
} catch Error(string memory reason) {
// catch failing revert() and require()
emit Log(reason);
} catch (bytes memory reason) {
// catch failing assert()
emit LogBytes(reason);
}
}
}

例子2

在solidity中,try-catch只能被用于external函数或创建合约时constructor(被视为external函数)的调用。基本语法如下:

try externalContract.f() {
// call成功的情况下 运行一些代码
} catch {
// call失败的情况下 运行一些代码
}

其中externalContract.f()是某个外部合约的函数调用,try模块在调用成功的情况下运行,而catch模块则在调用失败时运行。

同样可以使用this.f()来替代externalContract.f(),this.f()也被视作为外部调用,但不可在构造函数中使用,因为此时合约还未创建。

如果调用的函数有返回值,那么必须在try之后声明returns(returnType val),并且在try模块中可以使用返回的变量;如果是创建合约,那么返回值是新创建的合约变量。

try externalContract.f() returns(returnType val){
// call成功的情况下 运行一些代码
} catch {
// call失败的情况下 运行一些代码
}

另外,catch模块支持捕获特殊的异常原因:

try externalContract.f() returns(returnType){
// call成功的情况下 运行一些代码
} catch Error(string memory reason) {
// 捕获失败的 revert() 和 require()
} catch (bytes memory reason) {
// 捕获失败的 assert()
}

1.try-catch实战:

1-1.OnlyEven

我们创建一个外部合约OnlyEven,并使用try-catch来处理异常:

contract OnlyEven{
constructor(uint a){
require(a != 0, "invalid number");
assert(a != 1);
}

function onlyEven(uint256 b) external pure returns(bool success){
// 输入奇数时revert
require(b % 2 == 0, "Ups! Reverting");
success = true;
}
}

OnlyEven合约包含一个构造函数和一个onlyEven函数。

  • 构造函数有一个参数a,当a=0时,require会抛出异常;当a=1时,assert会抛出异常;其他情况均正常。
  • onlyEven函数有一个参数b,当b为奇数时,require会抛出异常。

1-2.处理外部函数调用异常

首先,在TryCatch合约中定义一些事件和状态变量:

// 成功event
event SuccessEvent();

// 失败event
event CatchEvent(string message);
event CatchByte(bytes data);

// 声明OnlyEven合约变量
OnlyEven even;

constructor() {
even = new OnlyEven(2);
}

SuccessEvent是调用成功会释放的事件,而CatchEvent和CatchByte是抛出异常时会释放的事件,分别对应require/revert和assert异常的情况。even是个OnlyEven合约类型的状态变量。

然后我们在execute函数中使用try-catch处理调用外部函数onlyEven中的异常:

// 在external call中使用try-catch
function execute(uint amount) external returns (bool success) {
try even.onlyEven(amount) returns(bool _success){
// call成功的情况下
emit SuccessEvent();
return _success;
} catch Error(string memory reason){
// call不成功的情况下
emit CatchEvent(reason);
}
}

1.error是solidity 0.8.4版本新加的内容

方便且高效(省gas)地向用户解释操作失败的原因,同时还可以在抛出异常的同时携带参数,帮助开发者更好地调试。

错误将撤消事务期间对状态所做的所有更改。 require您可以通过调用,revert或 来引发错误assert。

  • require用于在执行之前验证输入和条件。
  • revert类似于require。详情请参阅下面的代码。
  • assert用于检查不应该为假的代码。断言失败可能意味着存在错误。

An error will undo all changes made to the state during a transaction.

You can throw an error by calling require, revert or assert.

  • require is used to validate inputs and conditions before execution.
  • revert is similar to require. See the code below for details.
  • assert is used to check for code that should never be false. Failing assertion probably means that there is a bug.

使用自定义错误来节省gas。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Error {
function testRequire(uint _i) public pure {
// Require should be used to validate conditions such as:
// - inputs
// - conditions before execution
// - return values from calls to other functions
require(_i > 10, "Input must be greater than 10");
}

function testRevert(uint _i) public pure {
// Revert is useful when the condition to check is complex.
// This code does the exact same thing as the example above
if (_i <= 10) {
revert("Input must be greater than 10");
}
}

uint public num;

function testAssert() public view {
// Assert should only be used to test for internal errors,
// and to check invariants.

// Here we assert that num is always equal to 0
// since it is impossible to update the value of num
assert(num == 0);
}

// custom error
error InsufficientBalance(uint balance, uint withdrawAmount);

function testCustomError(uint _withdrawAmount) public view {
uint bal = address(this).balance;
if (bal < _withdrawAmount) {
revert InsufficientBalance({balance: bal, withdrawAmount: _withdrawAmount});
}
}
}

这是另一个例子

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Account {
uint public balance;
uint public constant MAX_UINT = 2 ** 256 - 1;

function deposit(uint _amount) public {
uint oldBalance = balance;
uint newBalance = balance + _amount;

// balance + _amount does not overflow if balance + _amount >= balance
require(newBalance >= oldBalance, "Overflow");

balance = newBalance;

assert(balance >= oldBalance);
}

function withdraw(uint _amount) public {
uint oldBalance = balance;

// balance - _amount does not underflow if balance >= _amount
require(balance >= _amount, "Underflow");

if (balance < _amount) {
revert("Underflow");
}

balance -= _amount;

assert(balance <= oldBalance);
}
}

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

error TransferNotOwner(); // 自定义error

我们也可以定义一个携带参数的异常,来提示尝试转账的账户地址

error TransferNotOwner(address sender); // 自定义的带参数的error

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

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

function transferOwner1(uint256 tokenId, address newOwner) public {
if(_owners[tokenId] != msg.sender){
revert TransferNotOwner();
// revert TransferNotOwner(msg.sender);
}
_owners[tokenId] = newOwner;
}

3.Assert

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

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

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