在 Solidity 中,我们无法像 JavaScript 那样 console.log("...") 来查看运行状态。但我们有事件(Event)机制——既是合约的“日志打印工具”,也是链下交互的主要接口。
事件并不会改变合约状态,但会被记录进交易回执(transaction receipt),可供前端监听、后端索引、分析工具检索。因此它在实际开发中既是调试利器,也是业务接口。
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 指令,把数据写入交易回执(receipt)的 logs 字段。
结构如下图所示:
╔═══════════════════════════════╗
║ Event Log ║
╠═══════════════════════════════╣
║ topic[0] = keccak("Transfer(address,address,uint256)") ║
║ topic[1] = indexed from ║
║ topic[2] = indexed to ║
║ data = abi.encode(value) ║
╚═══════════════════════════════╝也就是说:
indexed 参数成为一个独立 topic(最多三个)你可以通过 RPC(如 eth_getLogs)查询这些信息。
对比项 | 状态变量(Storage) | 事件(Event Log) |
|---|---|---|
是否可读 | 合约内部和链上都可读 | 合约内部不可读,链下可监听 |
是否可写 | 可修改 | 只能触发一次,不可修改 |
是否影响合约状态 | ✅ 是 | ❌ 否 |
存储位置 | 状态树(State Trie) | 交易回执(Transaction Receipt) |
适用场景 | 记录当前状态或持久数据 | 记录操作行为或审计信息 |
查询难度 | 高效 | 需链下索引或过滤 topic |
Gas 成本 | 高(SLOAD/SSTORE 操作昂贵) | 较低(LOG 操作较便宜) |
contract.on("Transfer", (from, to, value) => {
console.log(`转账事件:${from} → ${to},数量:${value}`);
});使用 indexed 的好处是可以设过滤条件:
const filter = contract.filters.Transfer(null, myAddress);
const logs = await contract.queryFilter(filter, fromBlock, toBlock);Foundry 的测试框架提供 vm.expectEmit 来验证事件触发:
function testEmitTransfer() public {
vm.expectEmit(true, true, false, true);
emit Transfer(address(this), address(1), 100);
token.transfer(address(1), 100);
}参数含义是:
vm.expectEmit 必须在调用函数之前使用!
在开发合约时,你可以在关键位置插入事件作为调试信息:
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:
forge test -vvv你可以在调试过程中看到 emit 的事件和参数,非常直观!
场景 | 是否推荐用事件 | 理由 |
|---|---|---|
用户行为审计(如投票、质押) | ✅ 是 | 保留可验证的链上历史 |
状态变更通知(如转账、授权) | ✅ 是 | 便于前端监听链上变化 |
储存合约业务状态 | ❌ 否 | 应使用 |
调试开发逻辑 | ✅ 是 | 在本地环境下替代 |
查询当前合约信息 | ❌ 否 | 不应从 event 推导状态,应使用 view 函数 |
提供可过滤的用户行为事件(如转账人) | ✅ 是 | 配合 |
indexed 参数写入 topic 占用更多 gas,请合理权衡合约片段:
// 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):
function testEmitCountUpdated() public {
vm.expectEmit(true, false, false, true);
emit CountUpdated(address(this), 1);
counter.increment();
}执行测试:
➜ 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 和响应未知调用?