事件

以太坊存在一种日志功能。当智能合约运行时,可以触发某个事件。该事件的数据会作为日志被保存到以太坊区块链中。

客户端(dapp)可以通过以太坊网络的RPC接口监听事件的发出,从而实现回调的功能。

一个事件可以带上多个参数,这些参数以及发出事件的合约地址等信息,都会做为日志数据写入以太坊区块链。

事件可以声明在文件层级,也可以声明在合约层级。

事件的声明与发出示例如下:

Event.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

contract Event {
    // 定义一个事件 SomebodyCall,接受两个参数
    event SomebodyCall(string name, uint time);

    function call(string calldata name) public {
        // 发送事件 SomebodyCall
        emit SomebodyCall(name, block.timestamp);
    }
}

我们部署该合约后,调用 callRemix IDE 中查看发出的日志信息如下:

[
	{
    // 发出的合约地址
		"from": "0xf8e81D47203A594245E36C48e151709F0C19fBe8",
    // 索引
		"topic": "0x0e896679c2db63f7cc88a48d09b1f09e82b9575508982c01a9f64fed35552ac6",
    // 事件名
		"event": "SomebodyCall",
    // 两个参数
		"args": {
			"0": "Mathew",
			"1": "1710299542",
			"name": "Mathew",
			"time": "1710299542"
		}
	}
]

我们这里是直接在 Remix IDE 中查看。一个典型的交互流程是:

  1. 客户端监听事件
  2. 用户通过客户端调用合约函数
  3. 合约函数执行,并发出事件
  4. 客户端收到事件并做出处理

对于不同的客户端,如 web3 ethers,连接和监听事件的方法不同,这里不做示例。

事件索引

为了方便监听时,对事件进行过滤和索引查找,我们可以为一个事件添加最多四个索引。事件的索引在日志中,称为 topics(话题)。

对于我们上面命名的事件:

event SomebodyCall(string name, uint time);

我们还没有为它指定任何索引,但是我们看上面的日志输出却有一个:

"topic": "0x0e896679c2db63f7cc88a48d09b1f09e82b9575508982c01a9f64fed35552ac6"

这是因为事件的默认第一个索引,是事件签名经过 hash 之后的值,例如上面的 topic 值是通过以下方式计算的:

keccak256("SomebodyCall(string,uint256)")

由于默认的事件签名索引占用了一个索引名额,因此我们实际上只能额外多定义三个索引。我们使用 indexed 来指明希望用作索引的参数:

Event2.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

contract Event {
    // 定义一个事件 SomebodyCall,接受两个参数,都指定为索引
    event SomebodyCall(string indexed name, uint256 indexed time);

    function call(string calldata name) public {
        // 发送事件 SomebodyCall
        emit SomebodyCall(name, block.timestamp);
    }
}

由于每一个 topic 大小不能超过 256 位,因此像 string 这种引用类型,会通过 keccak256 hash256 位。

匿名事件

我们可以通过在事件名称后面加上 anonymous 标识符,来声明一个匿名事件。

EventAnonymous.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

contract Event {
    // 定义一个事件 SomebodyCall,接受两个参数,指定为索引
    event SomebodyCall(string indexed name, uint256 indexed time) anonymous;

    function call(string calldata name) public {
        // 发送事件 SomebodyCall
        emit SomebodyCall(name, block.timestamp);
    }
}

匿名事件不会有默认的事件签名 topic,因此匿名事件支持最多声明四个索引参数,而且匿名参数发送的 gas 耗费更低。

由于没有了事件签名的 topic,因此如果可能出现事件混淆的情况,开发人员应当自行考虑如何进行区分。

事件重载

Solidity 还支持事件重载,即可以有同名的事件,只要事件接受的参数不同:

EventOverload.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

contract Event {
    // 事件重载
    event SomebodyCall(string indexed name);
    event SomebodyCall(string indexed name, uint256 indexed time) anonymous;

    function call(string calldata name) public {
        // 发送事件 SomebodyCall
        emit SomebodyCall(name, block.timestamp);
    }
}