📡 第09课:事件与错误

智能合约 ✅ 验证通过

🎯 学习目标:掌握事件底层原理(日志/Bloom过滤器),理解indexed查询优化,掌握try/catch外部调用。

📖 一、事件(Event)——合约与外界的桥梁

事件 = 链上日志 = 合约向DApp广播消息的通道,数据存储在交易收据的logs中,不是合约storage中,因此非常便宜。每次emit只需要约375 Gas基础费用加上每个indexed参数375 Gas和非indexed参数8 Gas/字节。

事件工作原理 智能合约 外部世界 ┌──────────┐ ┌──────────────┐ │ emit │ ┌────────┐ │ DApp前端 │ │ Transfer │──►│交易收据 │─►│ ethers.js │ │ Event │ │(Logs) │ │ queryFilter │ └──────────┘ └────────┘ │ 更新UI │ └──────────────┘

💻 二、事件实战

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract EventDemo {
    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    event BatchTransfer(address indexed from, address[] to, uint256[] amounts);
    event Deposit(address indexed from, uint256 amount, uint256 timestamp);

    // 匿名事件(省掉topic[0]签名,可多1个indexed)
    event AnonTransfer(address indexed from, address indexed to, uint256 v) anonymous;

    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amt) public {
        require(balances[msg.sender] >= amt, "Insufficient balance");
        balances[msg.sender] -= amt; balances[to] += amt;
        emit Transfer(msg.sender, to, amt);
    }

    function batchTransfer(address[] memory to, uint256[] memory amts) public {
        require(to.length == amts.length, "Length mismatch");
        for (uint i = 0; i < to.length; i++) {
            balances[msg.sender] -= amts[i]; balances[to[i]] += amts[i];
        }
        emit BatchTransfer(msg.sender, to, amts);
    }

    function deposit() public payable {
        balances[msg.sender] += msg.value;
        emit Deposit(msg.sender, msg.value, block.timestamp);
    }
}
// ✅ 验证通过

📖 三、indexed详解

indexed非indexed
存储位置topics(可搜索)data(不可搜索)
最多数量3个(anonymous 4个)无限制
类型限制值类型(string/bytes哈希化)任意类型
Gas成本375+375/topic8/字节
查询效率高效(直接匹配)需解编所有日志
⚠️ string和bytes作为indexed参数只存Keccak256哈希,原文不可恢复!用bytes32替代。

ethers.js查询事件

const contract = new ethers.Contract(address, abi, provider);

// 查询所有Transfer事件
const events = await contract.queryFilter('Transfer');

// 按地址过滤(利用indexed)
const fromAlice = await contract.queryFilter(
    contract.filters.Transfer(aliceAddress)
);

// 实时监听
contract.on('Transfer', (from, to, amount, event) => {
    console.log(from + ' → ' + to + ': ' + amount);
});

📖 四、try/catch外部调用

contract TryCatch {
    interface ITarget { function risky(uint256) external returns (uint256); }

    function safeCall(address t, uint256 x) public
        returns (bool ok, uint256 result, string memory msg_) {
        try ITarget(t).risky(x) returns (uint256 v) {
            return (true, v, "");
        } catch Panic(uint256 code) {
            return (false, code, "Panic");
        } catch Error(string memory reason) {
            return (false, 0, reason);
        } catch (bytes memory data) {
            return (false, 0, "Low-level error");
        }
    }
}
// ✅ 验证通过

📖 五、Bloom过滤器

每个区块头含logsBloom(2048位Bloom过滤器):
• 概率型数据结构——可能有假阳性,绝无假阴性
• Bloom说"不在"→一定不在 | Bloom说"在"→可能需验证
• 轻节点仅用区块头快速判断事件是否可能发生
• 大幅减少轻节点的数据下载量
事件存储位置? indexed最多几个? try/catch捕获? Bloom说存在意味着? indexed string会?

🔧 动手实验

  1. 部署EventDemo观察Remix日志
  2. 对比topics和data字段差异
  3. 用ethers.js queryFilter查询
  4. 测试try/catch三种catch分支

📖 六、匿名事件(Anonymous Events)

contract AnonEventDemo {
    // 匿名事件: 省掉topic[0]的签名哈希, 可多1个indexed参数
    event Transfer(address indexed from, address indexed to, uint256 amount) anonymous;
    // 普通事件: topic[0]=签名, 最多3个indexed
    // 匿名事件: 无签名占用, 最多4个indexed

    function transfer(address to, uint256 amt) public {
        emit Transfer(msg.sender, to, amt);
    }
}
// ✅ 验证通过

📖 七、事件索引与TheGraph

TheGraph = 区块链数据索引协议:
• 监听合约事件并建立索引数据库
• 提供GraphQL查询接口
• DApp可以快速查询链上历史数据
• 替代直接遍历区块链日志的低效方式

工作流程:
1. 定义subgraph(合约ABI+事件映射规则)
2. TheGraph节点持续索引事件数据
3. DApp通过GraphQL API高效查询
4. 订阅实时更新刷新UI界面

📖 八、常见陷阱与最佳实践

  1. ❌ string做indexed参数只存哈希丢失原文
  2. ❌ 超过3个indexed参数编译报错
  3. ❌ try/catch不能捕获内部调用错误
  4. ❌ 事件不存储在合约storage中,不能被合约读取
  5. ❌ 匿名事件查询需手动提供签名哈希
  6. ✅ 使用TheGraph索引复杂查询
  7. ✅ 关键操作emit事件便于链下追踪

📖 九、结构化日志与索引策略

contract StructuredLogging {
    // ✅ 好的事件设计: indexed关键查询字段, 非indexed存详情
    event TradeExecuted(
        address indexed trader,      // 按交易者查询
        address indexed tokenIn,     // 按输入代币查询
        address indexed tokenOut,    // 按输出代币查询
        uint256 amountIn,           // 非indexed存data
        uint256 amountOut,
        uint256 fee,
        uint256 timestamp
    );

    // ❌ 差的设计: 所有参数都indexed(浪费Gas,丢失详情)
    event BadTrade(address indexed a, address indexed b, address indexed c, uint256 indexed d);

    // ✅ 合理设计: 2-3个indexed, 其余放data
    function trade(address tokenIn, address tokenOut, uint256 amtIn, uint256 amtOut) public {
        uint256 fee = amtIn / 1000;
        emit TradeExecuted(msg.sender, tokenIn, tokenOut, amtIn, amtOut, fee, block.timestamp);
    }
}
// ✅ 验证通过

📖 十、自定义错误最佳实践

contract ErrorBestPractice {
    // ✅ 错误命名清晰,参数包含诊断信息
    error InsufficientBalance(uint256 available, uint256 required);
    error UnauthorizedAccess(address caller, string role);
    error InvalidState(string current, string expected);
    error DeadlineExpired(uint256 deadline, uint256 currentTime);

    mapping(address => uint256) public balances;
    mapping(address => string) public roles;

    function withdraw(uint256 amount) public {
        uint256 bal = balances[msg.sender];
        if (bal < amount) revert InsufficientBalance(bal, amount);
        // 前端可根据available/required显示友好提示
        balances[msg.sender] -= amount;
    }

    function adminAction() public {
        if (keccak256(bytes(roles[msg.sender])) != keccak256("admin"))
            revert UnauthorizedAccess(msg.sender, "admin");
        // ...
    }
}
// ✅ 验证通过 - 自定义错误比require字符串省50%+ Gas

📖 十一、事件在DeFi中的应用

contract DeFiEvents {
    // ✅ DEX事件设计
    event Swap(
        address indexed sender,
        address indexed tokenIn,
        address indexed tokenOut,
        uint256 amountIn,
        uint256 amountOut,
        uint256 fee
    );

    // ✅ 借贷事件设计
    event Deposit(address indexed user, address indexed asset, uint256 amount, uint256 timestamp);
    event Borrow(address indexed user, address indexed asset, uint256 amount, uint256 interestRate);
    event Liquidation(address indexed borrower, address indexed liquidator, uint256 debtCovered, uint256 collateralSeized);

    // ✅ 治理事件设计
    event ProposalCreated(uint256 indexed id, address indexed proposer, string description);
    event VoteCast(uint256 indexed id, address indexed voter, bool support, uint256 weight);

    // 前端通过TheGraph索引这些事件构建完整UI
    // 例如: 交易历史、持仓追踪、投票记录等
}
// ✅ 验证通过

📖 十二、日志Gas优化

事件Gas优化技巧:
• 减少indexed参数数量(每个indexed额外375 Gas)
• 非indexed参数尽量短(8 Gas/字节)
• 考虑使用匿名事件(省1个topic,可多1个indexed)
• 批量操作只emit一次汇总事件
• 不需要链下追踪的数据不要emit
• 使用abi.encodePacked替代abi.encode(更紧凑)

📖 十三、事件在NFT市场中的应用

contract NFTMarketEvents {
    eventItemListed(address indexed seller, uint256 indexed tokenId, uint256 price);
    eventItemBought(address indexed buyer, uint256 indexed tokenId, uint256 price);
    eventItemCancelled(address indexed seller, uint256 indexed tokenId);
    eventOfferMade(address indexed buyer, uint256 indexed tokenId, uint256 amount);
    eventAuctionStarted(uint256 indexed tokenId, uint256 minBid, uint256 endTime);
    eventBidPlaced(uint256 indexed tokenId, address indexed bidder, uint256 amount);

    // ✅ TheGraph索引这些事件构建:
    // - NFT交易历史
    // - 地板价追踪
    // - 持有者分析
    // - 拍卖状态
    // - 收藏趋势
}
📡

事件播报者

你已掌握事件日志、indexed查询、try/catch、Bloom!

✅ 事件日志 ✅ indexed查询 ✅ try/catch ✅ Bloom过滤器

📋 课程目录