事件(Event)是 Solidity 中一种重要的日志记录机制,用于在区块链上记录特定操作的发生,供外部应用监听和查询。
事件的基本概念
定义:事件是合约与外部世界通信的方式,将数据写入区块链日志,供 DApp 前端监听。
特点:
- 数据存储在交易收据的日志中,不占用合约 storage
- 比 storage 存储便宜得多(每个 topic 消耗 375 Gas,每个数据字节 8 Gas)
- 可以被前端应用通过 RPC 订阅和监听
- 支持索引参数,便于高效过滤查询
事件的基本用法
soliditycontract EventExample { // 定义事件 event Transfer(address indexed from, address indexed to, uint256 amount); event Approval(address indexed owner, address indexed spender, uint256 value); event Log(string message); function transfer(address to, uint256 amount) public { // 执行转账逻辑... // 触发事件 emit Transfer(msg.sender, to, amount); } function approve(address spender, uint256 value) public { // 执行授权逻辑... emit Approval(msg.sender, spender, value); } }
索引参数(Indexed Parameters)
最多可以对 3 个参数使用 indexed 关键字,这些参数会被存储为 topics,便于过滤查询。
soliditycontract IndexedEventExample { // indexed 参数最多 3 个 event Transfer( address indexed from, // topic[1] address indexed to, // topic[2] uint256 amount // data ); // 3 个 indexed 参数 event OrderCreated( bytes32 indexed orderId, // topic[1] address indexed buyer, // topic[2] address indexed seller, // topic[3] uint256 amount, // data uint256 timestamp // data ); function createOrder(address seller, uint256 amount) public { bytes32 orderId = keccak256(abi.encodePacked(msg.sender, block.timestamp)); emit OrderCreated(orderId, msg.sender, seller, amount, block.timestamp); } }
事件的 Gas 成本分析
| 操作 | Gas 成本 |
|---|---|
| 基础事件成本 | 375 Gas |
| 每个 indexed topic | 375 Gas |
| 每个数据字节 | 8 Gas |
| 存储到 storage(32 字节) | 20,000 Gas |
对比示例:
soliditycontract GasComparison { // 方式 1:使用 storage(昂贵) struct Transaction { address from; address to; uint256 amount; uint256 timestamp; } Transaction[] public transactions; function recordWithStorage(address to, uint256 amount) public { transactions.push(Transaction(msg.sender, to, amount, block.timestamp)); // Gas 成本:约 20,000+ Gas } // 方式 2:使用事件(便宜) event TransactionRecorded( address indexed from, address indexed to, uint256 amount, uint256 timestamp ); function recordWithEvent(address to, uint256 amount) public { emit TransactionRecorded(msg.sender, to, amount, block.timestamp); // Gas 成本:约 1,000-2,000 Gas } }
Gas 优化技巧
1. 合理使用 indexed 参数
soliditycontract IndexedOptimization { // 不推荐:对不需要过滤的大字段使用 indexed event BadEvent(string indexed largeData); // 浪费 Gas // 推荐:只对需要过滤的地址或 ID 使用 indexed event GoodEvent( address indexed user, // 需要按用户过滤 uint256 indexed itemId, // 需要按物品过滤 string description // 不需要过滤,不使用 indexed ); }
2. 减少事件参数数量
soliditycontract ParameterOptimization { // 不推荐:包含过多参数 event VerboseEvent( address user, uint256 amount, uint256 timestamp, uint256 blockNumber, bytes32 txHash, string metadata ); // 推荐:只包含必要信息 event OptimizedEvent( address indexed user, uint256 amount, string metadata ); // 时间戳和区块号可以通过交易信息获取 }
3. 使用匿名事件
匿名事件不使用事件签名作为 topic[0],可以节省 Gas。
soliditycontract AnonymousEvent { // 匿名事件,节省一个 topic event QuickLog(address indexed user, uint256 amount) anonymous; function quickLog(uint256 amount) public { emit QuickLog(msg.sender, amount); // 节省约 375 Gas } }
4. 批量事件 vs 单个事件
soliditycontract BatchEvent { // 方式 1:逐个触发事件(Gas 高) event SingleTransfer(address indexed to, uint256 amount); function batchTransferV1(address[] calldata recipients, uint256[] calldata amounts) public { for (uint i = 0; i < recipients.length; i++) { // 执行转账... emit SingleTransfer(recipients[i], amounts[i]); // 每个事件约 375+ Gas } } // 方式 2:批量事件(更优) event BatchTransfer(address[] recipients, uint256[] amounts); function batchTransferV2(address[] calldata recipients, uint256[] calldata amounts) public { // 执行批量转账... emit BatchTransfer(recipients, amounts); // 单个事件,更省 Gas } }
事件的实际应用场景
1. ERC20 Token 标准事件
solidityinterface IERC20 { event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); }
2. DeFi 协议事件
soliditycontract DeFiProtocol { event Deposit( address indexed user, address indexed token, uint256 amount, uint256 shares ); event Withdraw( address indexed user, address indexed token, uint256 amount, uint256 shares ); event RewardClaimed( address indexed user, address indexed rewardToken, uint256 amount ); function deposit(address token, uint256 amount) external { // 存款逻辑... uint256 shares = calculateShares(amount); emit Deposit(msg.sender, token, amount, shares); } }
前端监听事件
javascript// 使用 ethers.js 监听事件 const contract = new ethers.Contract(address, abi, provider); // 监听所有 Transfer 事件 contract.on("Transfer", (from, to, amount, event) => { console.log(`Transfer: ${from} -> ${to}, Amount: ${amount}`); }); // 过滤特定地址的事件 const filter = contract.filters.Transfer(userAddress, null); contract.on(filter, (from, to, amount, event) => { console.log(`Outgoing transfer from ${from}`); }); // 查询历史事件 const events = await contract.queryFilter("Transfer", fromBlock, toBlock);
最佳实践
- 对关键操作触发事件:所有状态变更都应该有对应的事件
- 合理使用 indexed:只对需要过滤查询的参数使用 indexed
- 事件命名规范:使用过去时态(如 Transfer、Approved)
- 包含完整信息:事件中包含足够的信息,避免前端需要额外查询
- Gas 优化:对于高频操作,考虑批量事件或匿名事件
- 文档化:在合约文档中说明所有事件及其用途