首页
学习
活动
专区
圈层
工具
发布
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》第 9 课:Solidity 事件与日志机制 —— 合约世界的“printf”工具

在 Solidity 中,我们无法像 JavaScript 那样 console.log("...") 来查看运行状态。但我们有事件(Event)机制——既是合约的“日志打印工具”,也是链下交互的主要接口。

事件并不会改变合约状态,但会被记录进交易回执(transaction receipt),可供前端监听、后端索引、分析工具检索。因此它在实际开发中既是调试利器,也是业务接口。


一、事件的声明与触发

代码语言:txt
复制
event Transfer(address indexed from, address indexed to, uint256 value);

function transfer(address to, uint256 value) public {
    // ...逻辑省略
    emit Transfer(msg.sender, to, value);
}

关键要素:

  • 使用 event 关键字定义
  • emit 触发事件(必须要显式调用)
  • indexed 表示该参数将存入 topics 中(最多三个 indexed 参数)

二、事件结构:EVM 中的 log 构成

当事件被触发时,EVM 会执行 LOG 指令,把数据写入交易回执(receipt)的 logs 字段。

结构如下图所示:

代码语言:txt
复制
╔═══════════════════════════════╗
║           Event Log           ║
╠═══════════════════════════════╣
║ topic[0] = keccak("Transfer(address,address,uint256)")   ║
║ topic[1] = indexed from        ║
║ topic[2] = indexed to          ║
║ data = abi.encode(value)       ║
╚═══════════════════════════════╝

也就是说:

  • 事件名与参数类型哈希(topic[0]) 是事件唯一标识
  • 每个 indexed 参数成为一个独立 topic(最多三个)
  • 剩余非 indexed 参数打包进 data 段

你可以通过 RPC(如 eth_getLogs)查询这些信息。


三、事件与状态变量的对比

对比项

状态变量(Storage)

事件(Event Log)

是否可读

合约内部和链上都可读

合约内部不可读,链下可监听

是否可写

可修改

只能触发一次,不可修改

是否影响合约状态

✅ 是

❌ 否

存储位置

状态树(State Trie)

交易回执(Transaction Receipt)

适用场景

记录当前状态或持久数据

记录操作行为或审计信息

查询难度

高效

需链下索引或过滤 topic

Gas 成本

高(SLOAD/SSTORE 操作昂贵)

较低(LOG 操作较便宜)


四、链下监听方式(Ethers.js)

代码语言:js
复制
contract.on("Transfer", (from, to, value) => {
  console.log(`转账事件:${from} → ${to},数量:${value}`);
});

使用 indexed 的好处是可以设过滤条件:

代码语言:js
复制
const filter = contract.filters.Transfer(null, myAddress);
const logs = await contract.queryFilter(filter, fromBlock, toBlock);

五、Foundry 中断言事件

Foundry 的测试框架提供 vm.expectEmit 来验证事件触发:

代码语言:txt
复制
function testEmitTransfer() public {
    vm.expectEmit(true, true, false, true);
    emit Transfer(address(this), address(1), 100);

    token.transfer(address(1), 100);
}

参数含义是:

  • 第一个布尔值:是否校验 topic[1]
  • 第二个布尔值:是否校验 topic[2]
  • 第三个布尔值:是否校验 topic[3]
  • 第四个布尔值:是否校验 data 区域

vm.expectEmit 必须在调用函数之前使用!


六、事件调试技巧

在开发合约时,你可以在关键位置插入事件作为调试信息:

代码语言:txt
复制
event DebugUint(string tag, uint value);
event DebugAddr(string tag, address addr);

function foo() public {
    emit DebugUint("step1", 42);
    emit DebugAddr("caller", msg.sender);
}

结合 Foundry 可视化 log:

代码语言:bash
复制
forge test -vvv

你可以在调试过程中看到 emit 的事件和参数,非常直观!


七、设计建议:事件的最佳实践

场景

是否推荐用事件

理由

用户行为审计(如投票、质押)

✅ 是

保留可验证的链上历史

状态变更通知(如转账、授权)

✅ 是

便于前端监听链上变化

储存合约业务状态

❌ 否

应使用 storage 变量

调试开发逻辑

✅ 是

在本地环境下替代 console.log 使用

查询当前合约信息

❌ 否

不应从 event 推导状态,应使用 view 函数

提供可过滤的用户行为事件(如转账人)

✅ 是

配合 indexed 提供高效 topic 查询


八、事件的限制

  1. 事件不可链上读取
    • Solidity 合约内无法读取历史事件
    • 不应将事件作为状态依据
  2. 事件无法修改
    • 一经触发不可撤回或更改
  3. 过度依赖 indexed 会影响 gas
    • indexed 参数写入 topic 占用更多 gas,请合理权衡

九、练习:实现并测试自定义事件

合约片段:

代码语言:txt
复制
// SPDX-License-Identifier: MIT
contract Counter {
    uint public count;

    event CountUpdated(address user, uint newCount);

    function increment() public {
        count += 1;
        emit CountUpdated(msg.sender, count);
    }
}

测试片段(Foundry):

代码语言:txt
复制
function testEmitCountUpdated() public {
    vm.expectEmit(true, false, false, true);
    emit CountUpdated(address(this), 1);
    counter.increment();
}

执行测试:

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

Ran 1 test for test/Counter.t.sol:CounterTest
[PASS] testIncrementEmitsEvent() (gas: 34318)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.19ms (863.38µs CPU time)

Ran 1 test suite in 194.02ms (4.19ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

十、进阶阅读


下一课预告

第 10 课:Solidity fallback / receive 函数 —— 合约如何收 ETH 和响应未知调用?

下一篇
举报
领券