复制成功

分享至

主页 > 数字货币 >

深入了解Solidity事件——Event

2024.02.27

来源:登链社区

在今天的文章中,我们将看一下 Solidity event,在更通用的以太坊和 EVM 中称为logs。我们将看到如何使用它们,它们的定义以及如何使用事件主题哈希和签名来过滤日志,以及关于何时应该使用这些的一些建议。

我们还将涵盖 检查-事件-交互 模式,这种著名的模式传统上应用于状态变量的重入,但我们将看到为什么这样的模式也应该应用于触发事件以及涉及的潜在风险和安全漏洞。

如何在 Solidity 中定义事件?

可以使用event关键字在 Solidity 中定义事件,如下所示。

interface ILight {
   event SwitchedON();
   event SwitchedOFF();
   event BulbReplaced();
}

你可以通过完全限定的访问合约名称,后跟.和事件名称来从另一个合约中访问事件,如下所示:

event RegisteredSuccessfully(address user)

事件签名将是:

event RegisteredSuccessfully(address user)

事件主题哈希将是:

bytes32 topicHash = RegisteredSuccessfully.selector;

请注意,只有 Solidity v0.8.15 以后,事件的 .selector 成员才能使用。

如果你查看发出的任何区块链日志,你会发现日志的主题的索引0(第一个)条目的对应于事件主题哈希。由于主题是能通过日志进行搜索的内容,因此我们可以用事件主题哈希能进行过滤:

在特定地址的智能合约内搜索特定事件。

在区块链上的所有合约中搜索特定事件。

我们将在下面进一步看到,anonymous 匿名事件是此规则的例外。anonymous关键字使它们不可搜索,因此使用术语“匿名”

基于这一事实,我们还可以推断,Solidity 中定义的最简单的事件,没有参数,比如上面定义的事件BulbReplacedSwitchedON,将在底层使用 LOG1 操作码来触发日志中的主题,因为事件本身是可搜索的。

可以添加更多的主题,其他主题将使用LOG2LOG3LOG4LOG5,只要这些参数被标记为indexed。让我们在下一节中看一下索引参数。

事件参数和索引参数

事件可以接受任何类型的参数,包括值类型(uintNbytesNbooladdress...),structenum和用户定义的值类型。

根据我在写本文的研究,唯一不允许的类型是内部函数类型。外部函数类型是允许的,但内部函数类型不允许。举例来说,下面的代码将无法编译。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract AnonymousEvents {
   event SecretPasswordHashUpdated(bytes32 secretPasswordHash) anonymous;
}

如果事件声明为anonymous,在合约 ABI 中,事件的"anonymous"字段将标记为true

深入了解Solidity事件——Event
img https://github.com/ethereum/solidity/issues/13086

匿名事件的一个优点是,它使你的合约更便宜部署,并且在触发时 Gas方面也更便宜。

匿名事件的一个很好的用例是对于只有一个事件的合约。监听合约中的所有事件是有意义的,因为只有这一个事件将出现在事件日志中。订阅其名称是无关紧要的,因为只定义了一个单一事件来由合约发出。因此,你可以将事件定义为匿名,并订阅来自合约的所有事件日志,并确认它们都是相同的事件。

查看匿名事件在流行代码库中的使用示例,如在 DappHub 的 DS-Note 合约[7] 中。

深入了解Solidity事件——Event
img 来源代码[8]

我们可以在上面的代码片段中看到,由于事件声明为匿名,这使得可以定义第四个“indexed”参数。

请注意,由于匿名事件没有 bytes32 主题哈希,因此匿名事件不支持 .selector 成员。

使用 LOG 操作码在汇编中触发事件

深入了解Solidity事件——Event
img https://docs.soliditylang.org/en/v0.8.19/yul.html#evm-dialect

在汇编中触发事件是可能的,使用 logN 指令,该指令对应于 EVM 指令集中的操作码。

要在汇编中触发事件,你必须将要由事件发出的所有数据存储在 memory 中的特定位置。

一旦你将要由事件发出的数据存储在内存中,然后可以将以下参数指定给 logN 指令:

p = 从中开始获取数据的内存位置。基本上这是一个内存指针,或者是一个“偏移量”或“内存索引”,具体取决于你如何称呼它。

s = 你希望从 p 开始在事件中发出的字节数。

所有其他参数 t1t2t3t4 都是你希望成为可索引的事件参数。请注意这里有两个重要的事情:1)这些参数应该与你事件定义中以相同顺序定义的参数相同,2)这些参数应该放在内存中以获取数据。

下面的代码片段显示了如何在汇编中执行此操作。

<span ) 10px 10px / 40px no-repeat ;height: 30px;width: 100%;margin-bottom: -7px;border-radius: 5px;'>event ExampleEventAsm(bytes32 tokenId);

function _emitEventAssembly(bytes32 tokenId) internal{
   bytes32 topicHash = ExampleEventAsm.selector;

   assembly {
       let freeMemoryPointer := mload(0x40)
       mstore(freeMemoryPointer, topicHash)
       mstore(add(freeMemoryPointer, 32), tokenId)

       // emit the `ExampleEventAsm` event with 2 topics
       log2(
           freeMemoryPointer, // `p` = starting offset in memory
           64, // `s` = number of bytes in memory from `p` to include in the event data
           topicHash, // topic for filtering the event itself
           tokenId // 1st indexed parameter
       )
   }
}

事件的 gas 成本

深入了解Solidity事件——Event

所有记录操作码(LOG0LOG1LOG2LOG3LOG4)都需要消耗 gas。它们具有的参数(主题)越多,它们消耗的 gas 就越多。

深入了解Solidity事件——Event
image-20240226195203141

此外,像索引或数据大小等其他因素也会导致事件发出消耗更多 gas。

免责声明:数字资产交易涉及重大风险,本资料不应作为投资决策依据,亦不应被解释为从事投资交易的建议。请确保充分了解所涉及的风险并谨慎投资。OKEx学院仅提供信息参考,不构成任何投资建议,用户一切投资行为与本站无关。

加⼊OKEx全球社群

和全球数字资产投资者交流讨论

扫码加入OKEx社群

相关推荐

industry-frontier