Solidity Events 详解

Events 是 Solidity 中记录事件的工具,可以简单理解为日志。Events 的优点在于,一是能够利用较少的 Gas 就能将数据记录在区块链上,二是可以方便链下对链上数据进行监听。

代码示例

先来看一段简单的代码:

pragma solidity 0.8.10;

contract EventsDemo {
    // 定义 Events
    event Transfer(
        address indexed from, 
        address indexed to, 
        uint256 amount
    );

    function transfer(address to, uint256 amount) external {
        // 发送 Events
        emit Transfer(msg.sender, to, amount);
    }
}

在上述代码中,我们通过 event 关键字定义事件,通过 emit 关键字发送事件。这样,在调用 transfer 函数的时候,就会发送 Transfer 事件,将其数据记录在链上。

接下来我们实际部署一下,并调用 transfer 看看会发生什么。

调用 transfer 时的参数我们分别设为:

  • to → 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
  • amount → 123

调用成功后,查看 Etherscan 的 Logs 页面:

我们可以看到页面中有三部分,分别是:

  • Address
  • Topics
  • Data

其中 Address 是合约地址,这个很容易理解,那么剩下的两个字段分别代表什么呢?

基本数据结构

Events 打印出的日志中包含两种数据结构,分别是 topicdata。每个日志中最多可以包含 4 个 topicdata 则没有限制。topic 的第一个字段默认是事件签名,在上例中,Topics[0] 中的 0xddf25…b3ef 哈希值就是 Transfer 事件的签名:

keccak256(bytes("Transfer(address,address,uint256)"))

图示即为(图片来自于这里):

我们注意到,前面定义的 Transfer 事件中,fromto 字段都带有一个 indexed 关键字,它的作用就是将该字段列为 topic。由于最多限制 4 个 topic,且第一个 topic 默认是事件签名,因此在合约中定义事件的时候,最多只能有 3 个字段可以使用 indexed。那么 topic 到底有什么用呢?

将字段列为 topic 可以方便检索。例如,我们想要在链下监听 Transfer 事件,但是并不想监听每一笔事件,只想监听 from 是我的地址的事件,那么就必须将 from 设置为 topic,否则 data 类型的日志是没有办法做到的。

在上例中,我们将 fromto 设置为 indexed,因此我们可以在上图中看到,有三个 topic,分别是

  • 事件签名
  • from,这里是调用合约的地址
  • to

那么剩下的 data 部分就是 amount 字段的数据了。

data 的内容是将数据经过 abi-encoded 的结果。

我们在 Etherscan 上传代码,刷新页面:

这时 Etherscan 就可以识别出事件的 name,并且可以将 topicdata 数据解码。

Topic 的限制

前面我们说到,一个事件日志中,最多只能有 4 个 topic,也就是说最多有 3 个 indexed。同时,topic 本身也有长度限制,每个 topic 只能容纳 32 个字节的数据。那么像 string 或者 bytes 这种非定长的数据能否设置为 topic 呢?

答案是可以的,但是需要对其内容进行哈希。我们来看一段代码:

pragma solidity 0.8.10;

contract EventsDemo {
    event Message(
        address indexed from,
        address indexed to,
        string message
    );

    function sendMessage(address to, string memory message) public {
        emit Message(msg.sender, to, message);
    }
}

首先,我们先将 Message 事件中的 message 字段设置非 indexed,部署并调用,调用的参数分别是:

  • to → 0x5b38da6a701c568545dcfcb03fcb875f56beddc4
  • message → Hello World

结果为:

message 字段位于 data 中,可以被正常解码。

接下来,我们给 message 加上 indexed

event Message(
    address indexed from,
    address indexed to,
    string indexed message
);

部署并调用,使用同样的参数,结果为:

此时,data 中内容为空,而 topic[3] 的值就是 message 的哈希值:

keccak256(bytes("Hello World"))

Events 的 Gas 花费

根据以太坊黄皮书的内容,日志的基础费用是 375 Gas。另外每个 topic 同样需要花费 375 Gas,data 中每个字节需要花费 8 Gas。

因此,我们前面的 Transfer 事件花费的 Gas 为:

1756 = 375(基础费用)+ 375 * 3(3 个 topic)+ 32 * 8(data 中共 32 字节)

Low-level log

Solidity 老版本中还存在一种底层调用,例如:

pragma solidity >=0.4.10 <0.7.0;

contract C {
    function f() public payable {
        uint256 _id = 0x420042;
        log2(
            bytes32(msg.value),
            bytes32(uint256(msg.sender)),
            bytes32(_id)
        );
    }
}

包括 log0log1log2log3log4,不过新版文档中已经去除了这部分内容,因此我们也不再介绍,了解其存在即可,感兴趣的朋友可以查看这里

监听事件

链下监听事件有很多种方法,几乎所有主流语言都有对应的 web3 库可以实现。我前面写的一篇 ethers 使用教程的文章中包含了这部分内容,感兴趣的朋友可以前往查看,这里就不再赘述了。

总结

Solidity 中 Events 是一个很实用的日志工具,花费 Gas 少,利于链下来监听链上交易。

关于我

欢迎和我交流

参考

Subscribe to xyyme.eth
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.