首页
学习
活动
专区
圈层
工具
发布
49 篇文章
1
《纸上谈兵·solidity》第 0 课:搭建 Solidity 开发环境(三种方式)
2
《纸上谈兵·solidity》第 1 课:部署你的第一个 Solidity 合约
3
《纸上谈兵·solidity》第 2 课:调用、修改、读取,Solidity 合约不是 REST API
4
《纸上谈兵·solidity》第 3 课:事件(Event)机制与链上日志——不是 print,是广播!
5
《纸上谈兵·solidity》第 4 课:Solidity 合约中的错误处理机制(`require`、`revert`、`assert`)和自定义错误
6
《纸上谈兵·solidity》第 5 课:依赖与外部调用 —— 合约交互的风险与防护
7
《纸上谈兵·solidity》第 6 课:Solidity 数据存储布局 —— memory、storage、calldata 傻傻分不清?
8
《纸上谈兵·solidity》第 7 课:Solidity 函数可见性和修饰器 —— public 和 private 不只是权限标签
9
《纸上谈兵·solidity》第 8 课:Solidity 中的继承与接口 —— 模块化不是“复制粘贴”的借口
10
《纸上谈兵·solidity》第 9 课:Solidity 事件与日志机制 —— 合约世界的“printf”工具
11
《纸上谈兵·solidity》第 10 课:Solidity `fallback` / `receive` 函数 —— 合约如何收 ETH 和响应未知调用?
12
《纸上谈兵·solidity》第 11 课:Solidity 错误处理与异常机制 —— 让合约优雅地失败
13
《纸上谈兵·solidity》第 12 课:Solidity 函数选择器与 ABI 编码原理
14
《纸上谈兵·solidity》第 13 课:Solidity 低级调用 call/delegatecall/staticcall —— 直接和 EVM“对话”
15
《纸上谈兵·solidity》第 14 课:Solidity 中的可升级合约模式 —— 从代理合约到透明代理、UUPS 与安全陷阱
16
《纸上谈兵·solidity》第 15 课:Solidity 库与可重用代码
17
《纸上谈兵·solidity》第 16 课:Pull over Push 支付模式与 Check-Effects-Interactions 原则
18
《纸上谈兵·solidity》第 17 课:合约设计模式实战(二)—— Access Control 与权限管理
19
《纸上谈兵·solidity》第 18 课:合约设计模式实战(三)—— 代理 + 插件化架构(Diamond Standard / EIP-2535)
20
《纸上谈兵·solidity》第 19 课:安全专题(一)—— 常见攻击手法与防御
21
《纸上谈兵·solidity》第 20 课:Solidity 安全专题(二)—— 编译器特性与低级漏洞
22
《纸上谈兵·solidity》第 21 课:Gas 优化与成本分析 —— 写出便宜的智能合约
23
《纸上谈兵·solidity》第 22 课:代币合约(ERC20)从零实现与扩展
24
《纸上谈兵·solidity》第 23 课:NFT 合约(ERC721 / ERC1155)实战
25
《纸上谈兵·solidity》第 24 课:去中心化众筹合约(Crowdfunding)实战
26
《纸上谈兵·solidity》第 25 课:简化版的去中心化交易所(DEX)简化版
27
《纸上谈兵·solidity》第 26 课:借贷合约简化实现
28
《纸上谈兵·solidity》第 27 课:DAO 治理合约(去中心化自治组织)
29
《纸上谈兵·solidity》第 28 课:智能合约安全审计案例复盘 -- The DAO Hack(2016)
30
《纸上谈兵·solidity》第 29 课:智能合约安全审计案例复盘 -- Parity Wallet Hack(2017)
31
《纸上谈兵·solidity》第 30 课:智能合约安全审计案例复盘 -- Nomad Bridge(2022)
32
《纸上谈兵·solidity》第 31 课:多签钱包在跨链桥中的应用 —— Nomad 事件复盘
33
《纸上谈兵·solidity》第 32 课:DeFi 基础合约
34
《纸上谈兵·solidity》第 33 课:多签钱包(Multisig Wallet)-- 合约设计与实现
35
《纸上谈兵·solidity》第 34 课:多签钱包(Multisig Wallet)-- 上线
36
《纸上谈兵·solidity》第 35 课:去中心化交易所(DEX)实战 — 合约设计
37
《纸上谈兵·solidity》第 36 课:去中心化交易所(DEX)实战 — 上线
38
《纸上谈兵·solidity》第 37 课:DeFi 实战 -- 资金池与利率模型
39
《纸上谈兵·solidity》第 38 课:DeFi 实战(2) -- 清算机制与价格预言机
40
《纸上谈兵·solidity》第 39 课:DeFi 实战(3) -- 利息累积与 aToken 设计
41
《纸上谈兵·solidity》第 40 课:DeFi 实战(4) -- 风险控制与防护
42
《纸上谈兵·solidity》第 41 课:DeFi 实战(5) -- 协议费与治理
43
《纸上谈兵·solidity》第 42 课:DeFi 实战(6) -- 跨资产借贷与多市场支持
44
《纸上谈兵·solidity》第 43 课:DeFi 实战(7) -- 清算机制进阶(多资产抵押清算路径、拍卖机制)
45
《纸上谈兵·solidity》第 44 课:DeFi 实战(8) -- 利率曲线与资金池优化(动态利用率模型)
46
《纸上谈兵·solidity》第 45 课:DeFi 实战(9) -- 利息累积与结算机制(可复利)
47
《纸上谈兵·solidity》第 46 课:DeFi 实战(10) -- 跨链借贷与流动性桥接
48
《纸上谈兵·solidity》第 47 课:DeFi 实战(11) -- 治理代币 & 激励机制(Tokenomics & Governance)
49
《纸上谈兵·solidity》第 48 课:DeFi 实战(12) -- 前端 DApp 集成与用户交互(React + ethers.js 实战)

《纸上谈兵·solidity》第 11 课:Solidity 错误处理与异常机制 —— 让合约优雅地失败

在 Solidity 智能合约开发中,失败并不可怕,可怕的是失败后状态不明确、资金不安全、调用方摸不着头脑。EVM 的一个重要特性是:当合约执行中发生错误时,会回滚所有状态更改,并退还未使用的 Gas。因此,正确使用错误处理机制,能够让合约在异常情况下安全地停止,而不是留下一地鸡毛。


一、 三种主要的错误处理方式

语句

用途

特点

require(condition, "msg")

检查外部输入、函数前置条件

条件不满足时抛错并回滚,退还剩余 Gas,带错误信息

revert("msg")

主动触发错误并中断执行

常用于多层逻辑判断中提前退出

assert(condition)

检查内部不变量(invariant)

条件为 false 时触发 Panic 错误,消耗所有剩余 Gas,表示严重逻辑错误


二、 自定义错误(Custom Error)

Solidity 0.8.4 引入了 Custom Error,可以用来代替 require/revert 的字符串错误信息,优势是 更节省 Gas

代码语言:txt
复制
error Unauthorized(address caller);
error InsufficientBalance(uint256 available, uint256 required);

触发方法:

代码语言:txt
复制
if (msg.sender != owner) {
    revert Unauthorized(msg.sender);
}

三、 错误触发后的状态回滚

  • 原子性:Solidity 中一次交易内的所有状态修改要么全部生效,要么全部回滚。
  • 资金安全:如果中途发生 require/revert/assert 抛错,之前的转账、变量修改统统不生效。
  • 多步操作:需要考虑调用链上其他合约的回滚影响。

四、 Foundry 示例

src/Bank.sol

代码语言:txt
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Bank {
    mapping(address => uint256) public balances;
    address public owner;

    error Unauthorized(address caller);
    error InsufficientBalance(uint256 available, uint256 required);

    constructor() {
        owner = msg.sender;
    }

    function deposit() external payable {
        require(msg.value > 0, "Deposit must be > 0");
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external {
        uint256 bal = balances[msg.sender];
        if (bal < amount) {
            revert InsufficientBalance(bal, amount);
        }
        balances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
    }

    function emergencyWithdraw() external {
        if (msg.sender != owner) {
            revert Unauthorized(msg.sender);
        }
        payable(owner).transfer(address(this).balance);
    }

    function internalCheck() external pure {
        // 如果条件不满足,将触发 Panic(uint256) 错误
        assert(false);
    }
}

test/Bank.t.sol

代码语言:txt
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Bank.sol";

contract BankTest is Test {
    Bank bank;
    address user1 = address(0x123);
    address user2 = address(0x456);

    function setUp() public {
        bank = new Bank();
        vm.deal(user1, 5 ether);
        vm.deal(user2, 2 ether);
    }

    function testDeposit() public {
        vm.prank(user1);
        bank.deposit{value: 1 ether}();
        assertEq(bank.balances(user1), 1 ether);
    }

    function testWithdrawSuccess() public {
        vm.startPrank(user1);
        bank.deposit{value: 2 ether}();
        bank.withdraw(1 ether);
        assertEq(bank.balances(user1), 1 ether);
        vm.stopPrank();
    }

    function testWithdrawFail_CustomError() public {
        vm.prank(user1);
        vm.expectRevert(
            abi.encodeWithSelector(
                Bank.InsufficientBalance.selector,
                0,
                1 ether
            )
        );
        bank.withdraw(1 ether);
    }

    function testEmergencyWithdrawFail_Unauthorized() public {
        vm.prank(user2);
        vm.expectRevert(
            abi.encodeWithSelector(
                Bank.Unauthorized.selector,
                user2
            )
        );
        bank.emergencyWithdraw();
    }

    function testAssertPanic() public {
        vm.expectRevert(); // Panic(uint256) is a generic revert for assert
        bank.internalCheck();
    }
}

执行测试:

代码语言:bash
复制
➜  counter git:(main) ✗ forge test --match-path test/Bank.t.sol -vvv
[⠊] Compiling...
[⠒] Compiling 2 files with Solc 0.8.30
[⠑] Solc 0.8.30 finished in 528.49ms
Compiler run successful!

Ran 5 tests for test/Bank.t.sol:BankTest
[PASS] testAssertPanic() (gas: 8270)
[PASS] testDeposit() (gas: 42157)
[PASS] testEmergencyWithdrawFail_Unauthorized() (gas: 14240)
[PASS] testWithdrawFail_CustomError() (gas: 14957)
[PASS] testWithdrawSuccess() (gas: 51239)
Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 4.35ms (2.73ms CPU time)

Ran 1 test suite in 207.34ms (4.35ms CPU time): 5 tests passed, 0 failed, 0 skipped (5 total tests)

五、 安全建议

  1. 外部输入用 require 检查,防止无效参数进入业务逻辑。
  2. 多分支逻辑中可用 revert 提前退出,保持代码可读性。
  3. 关键不变量用 assert 保证,若断言失败说明合约存在漏洞。
  4. 推荐使用 Custom Error 代替字符串错误信息,节省部署和执行 Gas。
  5. 测试必须覆盖失败场景,验证合约在异常情况下的安全性和可预期行为。
下一篇
举报
领券