定义事件以及如何使用事件主题哈希和签名来过滤日志,以及关于何时应该使用事件的一些建议。 你知道 检查-事件-交互 模式么? 看看本篇文章
- 原文链接:https://medium.com/@jeancvllr/solidity-all-about-events-5bbe330f2513
- 译文出自:登链翻译计划
- 译者:翻译小组 ,校对:Tiny 熊
- 本文永久链接:learnblockchain.cn/article…
在今天的文章中,我们将看一下 Solidity event
,在更通用的以太坊和 EVM 中称为logs。我们将看到如何使用它们,它们的定义以及如何使用事件主题哈希和签名来过滤日志,以及关于何时应该使用这些的一些建议。
我们还将涵盖 检查-事件-交互 模式,这种著名的模式传统上应用于状态变量的重入,但我们将看到为什么这样的模式也应该应用于触发事件以及涉及的潜在风险和安全漏洞。
可以使用event
关键字在 Solidity 中定义事件,如下所示。
event RegisteredSuccessfully(address user)
当使用动态类型和多值类型,如bytes
,string
或类型为T[]
的数组时,在事件定义中不需要为参数指定数据位置。
Solidity 事件只能在合约、库或接口中定义。但是从 Solidity v0.8.22 以后,事件也可以在文件级别定义。
你还可以从另一个合约中访问在合约中定义的事件。从 Solidity v0.8.15 以后,可以从其他合约完全限定地访问事件。
你有又以下文件:
interface ILight {
event SwitchedON();
event SwitchedOFF();
event BulbReplaced();
}
你可以通过完全限定的访问合约名称,后跟.
和事件名称来从另一个合约中访问事件,如下所示:
import {ILight} from "ILight.sol";
contract LightHouse {
function lightTowardsDirections(uint256 latitude, uint256 longitude) public {
// code logic
emit ILight.SwitchedON();
}
}
你可以在函数中使用emit
关键字发出event
。
如果函数会触发事件,则不能将其定义为view
或pure
。这是因为触发事件会将数据写入区块链(到日志中)。
在 Solidity 中,事件的签名形式与函数签名相同。
它简单地对应于事件名称+用逗号,
分隔的参数类型,所有这些都用括号括起来。
使用我们之前的示例:
event RegisteredSuccessfully(address user)
事件签名将是:
RegisteredSuccessfully(address)
当监听合约的事件时,这些事件是使用事件主题哈希进行过滤的。
事件主题哈希对应于事件签名的keccak256哈希。
使用我们之前的示例:
event RegisteredSuccessfully(address user)
事件主题哈希将是:
keccak256("RegisteredSuccessfully(address)")
= 0x2a5fc519cb1ec56867d94a911a7ba739c06c6772c4841545feb12cea840ab90c
可以使用以下语法在 Solidity 中访问事件主题哈希。这将返回 32 字节的选择器主题。返回的类型确实是bytes32
。
bytes32 topicHash = RegisteredSuccessfully.selector;
请注意,只有 Solidity v0.8.15 以后,事件的 .selector
成员才能使用。
如果你查看发出的任何区块链日志,你会发现日志的主题的索引0
(第一个)条目的对应于事件主题哈希。由于主题是能通过日志进行搜索的内容,因此我们可以用事件主题哈希能进行过滤:
我们将在下面进一步看到,
anonymous
匿名事件是此规则的例外。anonymous
关键字使它们不可搜索,因此使用术语“匿名”。
基于这一事实,我们还可以推断,Solidity 中定义的最简单的事件,没有参数,比如上面定义的事件BulbReplaced
或SwitchedON
,将在底层使用 LOG1
操作码来触发日志中的主题,因为事件本身是可搜索的。
可以添加更多的主题,其他主题将使用LOG2
,LOG3
,LOG4
和LOG5
,只要这些参数被标记为indexed
。让我们在下一节中看一下索引参数。
事件可以接受任何类型的参数,包括值类型(uintN
,bytesN
,bool
,address
...),struct
,enum
和用户定义的值类型。
根据我在写本文的研究,唯一不允许的类型是内部函数类型。外部函数类型是允许的,但内部函数类型不允许。举例来说,下面的代码将无法编译。
// This is ok and valid
event SomeEvent(function () external callableFunction);
// This will not compile
event AnotherEvent(function () internal someInternalParameter);
Solidity 事件的参数可以指定为indexed
。在这种情况下,它可以根据此事件中发出的特定值来缩小过滤事件。
标准的 Solidityevent
可以包含最多 3 个indexed
参数。
定义为anonymous
的事件可以包含最多 4 个indexed
参数。
额外说明,任何用作事件参数的复杂类型,如struct
,enum
或用户定义的值类型,将会将参数转换为 ABI 中的关联值类型。例如:
struct
:作为在结构中指定类型的元组。enum
:作为uint8
与命名函数参数一样,可以使用对象{}
语法使用命名参数触发事件。这可以帮助提高可读性。
以LSP7DigitalAsset
在 LUKSO LSP 智能合约上的实现为例。下面的屏幕截图显示了内部_mint
函数中代码的一部分,强调了传递给Transfer
事件的参数。
这提供了更好的可读性和清晰度。
在 Solidity 和 EVM 中,事件可以标记为anonymous
。匿名事件在某种意义上是 Solidity 和 EVM 中的特殊事件,因为它们不能通过其名称进行过滤,因此不能直接监听。
Solidity 如下所述:
“…这意味着无法按名称过滤特定的匿名事件”
无法按名称过滤特定的匿名事件,你就只能按合约地址进行过滤。
例如,你可以监听智能合约发出的所有事件。这些匿名事件将出现在监听器中,但不能专门使用事件名称订阅它。这与其他事件不同。
你可以通过在事件定义(括号关闭后分号;
之前)之后和之前放置anonymous
关键字来在 Solidity 中将事件定义为匿名。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract AnonymousEvents {
event SecretPasswordHashUpdated(bytes32 secretPasswordHash) anonymous;
}
如果事件声明为anonymous
,在合约 ABI 中,事件的"anonymous"
字段将标记为true
。
匿名事件的一个优点是,它使你的合约更便宜部署,并且在触发时 Gas方面也更便宜。
匿名事件的一个很好的用例是对于只有一个事件的合约。监听合约中的所有事件是有意义的,因为只有这一个事件将出现在事件日志中。订阅其名称是无关紧要的,因为只定义了一个单一事件来由合约发出。因此,你可以将事件定义为匿名,并订阅来自合约的所有事件日志,并确认它们都是相同的事件。
查看匿名事件在流行代码库中的使用示例,如在 DappHub 的 DS-Note 合约 中。
来源代码
我们可以在上面的代码片段中看到,由于事件声明为匿名,这使得可以定义第四个“indexed”参数。
请注意,由于匿名事件没有 bytes32 主题哈希,因此匿名事件不支持 .selector
成员。
https://docs.soliditylang.org/en/v0.8.19/yul.html#evm-dialect
在汇编中触发事件是可能的,使用 logN
指令,该指令对应于 EVM 指令集中的操作码。
要在汇编中触发事件,你必须将要由事件发出的所有数据存储在 memory
中的特定位置。
一旦你将要由事件发出的数据存储在内存中,然后可以将以下参数指定给 logN 指令:
t1
、t2
、t3
和 t4
都是你希望成为可索引的事件参数。请注意这里有两个重要的事情:1)这些参数应该与你事件定义中以相同顺序定义的参数相同,2)这些参数应该放在内存中以获取数据。下面的代码片段显示了如何在汇编中执行此操作。
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
)
}
}
所有记录操作码(LOG0
、LOG1
、LOG2
、LOG3
、LOG4
)都需要消耗 gas。它们具有的参数(主题)越多,它们消耗的 gas 就越多。
此外,像索引或数据大小等其他因素也会导致事件发出消耗更多 gas。
检查-生效-交互模式也适用于事件。
一种检测这些模式的方法是使用 Remix 静态分析工具。
这种模式也可以被 Slither 检测到。当对一个在外部调用后触发事件的合约运行 slither 时,你将得到一个发现,提示 “重入事件”。
因此,对于 dApp 来说,顺序很重要,这样你就可以正确地查看哪个事件首先、接下来和最后被发出。这在递归或重入调用的情况下尤其重要。如果在外部调用后触发事件,并且这个外部调用进行了一个重入调用,那么:
理解这一点,也使得可以在链下提供清晰的审计跟踪,以监视合约调用。你可以看到哪些函数首先和最后被调用,以及在执行交易期间每个例程的运行顺序。
slither 检测器文档 - Solidity 和 Vyper 的静态分析器。
这种潜在的漏洞也在 Trail of Bits 对 Liquity 智能合约的审计中发现并报告。
在你的合约中可能有几种情况下触发事件可能很重要和有用。
transfer ownership (address)
函数,该函数只能由所有者调用以更改合约的所有者。Slither 检测器文档中描述了更多关于这些情况的信息。
这也在 Trail 对 LooksRare 的审计报告中描述了。
查看 0xprotocol 的详细信息,了解有关事件的安全相关问题。
[匿名事件的优势]
--
本翻译由 DeCert.me 协助支持, 在 DeCert 构建可信履历,为自己码一个未来。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!