在智能合约的开发过程中,错误处理 是确保系统健壮性、安全性和可预测行为的关键环节。本课我们将深入探讨 Solidity 中三种主要的错误处理机制:
require: 外部输入校验与逻辑前置判断revert: 复杂条件下的显式失败assert: 关键不变量的保护(程序性保证)区块链是不可逆的执行环境,一旦执行逻辑失败或出现异常,状态必须 完全回滚,以防止资金损失或系统进入不可恢复状态。Solidity 的错误处理机制允许我们:
举例:如果用户尝试从余额为 0 的账户中取款,我们应在逻辑层立即中止执行,而不是继续走到更底层逻辑。
require(condition, "Failure message");condition 为 false,自动 revert,并附带错误消息function deposit(uint amount) public {
require(amount > 0, "Deposit amount must be greater than zero");
balance[msg.sender] += amount;
}if (conditionFails) {
revert("Reason for failure");
}require 类似,都会 回滚状态并退还 Gas(未消耗部分)function withdraw(uint amount) public {
if (amount > balance[msg.sender]) {
revert("Insufficient balance");
}
balance[msg.sender] -= amount;
}function executeTrade(address user, uint amount) public {
if (!isWhitelisted(user)) {
revert("User not whitelisted");
}
if (amount < minTradeSize) {
revert("Trade amount too small");
}
// proceed with trade
}assert(condition);Panic(uint256) 错误(错误码如 0x01 表示断言失败)function increment() public {
count += 1;
assert(count > 0); // 这个永远应该成立
}unchecked)Solidity 0.8.4 起引入了自定义错误机制:
error NotEnoughBalance(uint256 requested, uint256 available);
function withdraw(uint amount) public {
if (amount > balance[msg.sender]) {
revert NotEnoughBalance(amount, balance[msg.sender]);
}
}项 | String 失败 | 自定义错误 |
|---|---|---|
Gas 成本 | 高(每个字符都编码) | 低(只编码数据) |
可读性 | 好 | 好 |
可结构化分析 | 不行 | 可以(可被前端或脚本解析) |
error Unauthorized(address caller);
error InvalidAmount(uint256 amount);
function transferOwnership(address newOwner) public {
if (msg.sender != owner) revert Unauthorized(msg.sender);
if (newOwner == address(0)) revert InvalidAmount(0);
owner = newOwner;
}场景 | 使用机制 | 推荐理由 |
|---|---|---|
基础参数验证 |
| 简洁明确,附带错误消息 |
多条件判断或嵌套逻辑 |
| 可读性好,适合逻辑封装 |
内部程序断言 |
| 表示不可被破坏的程序性不变量 |
节约 Gas + 可调试 |
| 结构清晰,适合高复杂度和可读性设计 |
合约我们还是以 Counter 为例,在这次的实验中,我们将使用 require 和 revert 来对 count 进行限制,确保它小于 10,以及使用 assert 来防止内部状态错误:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Counter {
uint256 public count;
// 添加事件
event Incremented(uint256 newCount);
function increment() public {
count += 1;
// 触发事件
emit Incremented(count);
}
function getCount() public view returns (uint256) {
return count;
}
function whoAmI() public view returns (address) {
return msg.sender;
}
}count 不能超过 10function incrementRequire() public {
require(count < 10, "Counter overflow");
count += 1;
}error CounterOverflow(uint current);
function incrementRevert() public {
if (count >= 10) {
revert CounterOverflow(count);
}
count += 1;
}assert 来检测内部异常function decrement() public {
count -= 1;
assert(count >= 0); // uint 永远不能 < 0,会 Panic
}编译时不会报错,但运行时可能触发 Panic(0x11) 错误。推荐加
require防御性判断替代。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Counter.sol";
contract CounterTest is Test {
Counter counter;
function setUp() public {
counter = new Counter();
}
function testInitialCountIsZero() view public {
uint256 c = counter.getCount();
assertEq(c, 0);
}
function testIncrementIncreasesCount() public {
counter.increment();
uint256 c = counter.getCount();
assertEq(c, 1);
}
// 测试事件是否正确触发
function testIncrementEmitsEvent() public {
// 设定期望的事件参数(按顺序匹配)
vm.expectEmit(false, false, false, true); // 只检查数据,不检查 topics(因为 uint256 不能 indexed)
emit Counter.Incremented(1);
counter.increment();
}
function testIncrementRequireSuccess() public {
for (uint i = 0; i < 10; i++) {
counter.incrementRequire();
}
}
function testIncrementRequireFail() public {
for (uint i = 0; i < 10; i++) {
counter.incrementRequire();
}
vm.expectRevert("Counter overflow");
counter.incrementRequire();
}
function testIncrementRevertCustomError() public {
for (uint i = 0; i < 10; i++) {
counter.incrementRevert();
}
vm.expectRevert(abi.encodeWithSelector(Counter.CounterOverflow.selector, 10));
counter.incrementRevert();
}
function testDecrementPanics() public {
vm.expectRevert(); // expect a panic due to underflow
counter.decrement();
}
function testDecrementAfterIncrement() public {
counter.increment();
counter.decrement();
assertEq(counter.getCount(), 0);
}
}requirerevertassert