
來源:登鏈社區
在今天的文章中,我們將看一下 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 中定義的最簡單的事件,沒有參數,比如上面定義的事件 BulbReplaced
或 SwitchedON
,將在底層使用 LOG1
操作碼來觸發日誌中的主題,因為事件本身是可搜索的。
可以添加更多的主題,其他主題將使用 LOG2
, LOG3
, LOG4
和 LOG5
,只要這些參數被標記為 indexed
。讓我們在下一節中看一下索引參數。
事件參數和索引參數
事件可以接受任何類型的參數,包括值類型( uintN
, bytesN
, bool
, address
…), struct
, enum
和用戶定義的值類型。
根據我在寫本文的研究,唯一不允許的類型是內部函數類型。外部函數類型是允許的,但內部函數類型不允許。舉例來說,下面的代碼將無法編譯。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract AnonymousEvents {
event SecretPasswordHashUpdated(bytes32 secretPasswordHash) anonymous;
}
如果事件聲明為 anonymous
,在合約 ABI 中,事件的 "anonymous"
欄位將標記為 true
。

匿名事件的一個優點是,它使你的合約更便宜部署,並且在觸發時 Gas方面也更便宜。
匿名事件的一個很好的用例是對於只有一個事件的合約。監聽合約中的所有事件是有意義的,因為只有這一個事件將出現在事件日誌中。訂閱其名稱是無關緊要的,因為只定義了一個單一事件來由合約發出。因此,你可以將事件定義為匿名,並訂閱來自合約的所有事件日誌,並確認它們都是相同的事件。
查看匿名事件在流行代碼庫中的使用示例,如在 DappHub 的 DS-Note 合約 [7] 中。

我們可以在上面的代碼片段中看到,由於事件聲明為匿名,這使得可以定義第四個「indexed」參數。
請注意,由於匿名事件沒有 bytes32 主題哈希,因此匿名事件不支持 .selector
成員。
使用 LOG 操作碼在彙編中觸發事件

在彙編中觸發事件是可能的,使用 logN
指令,該指令對應於 EVM 指令集中的操作碼。
要在彙編中觸發事件,你必須將要由事件發出的所有數據存儲在 memory
中的特定位置。
一旦你將要由事件發出的數據存儲在內存中,然後可以將以下參數指定給 logN 指令:
-
p = 從中開始獲取數據的內存位置。基本上這是一個內存指針,或者是一個「偏移量」或「內存索引」,具體取決於你如何稱呼它。
-
s = 你希望從 p 開始在事件中發出的字節數。
-
所有其他參數
t1
、t2
、t3
和t4
都是你希望成為可索引的事件參數。請注意這裡有兩個重要的事情: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 成本

所有記錄操作碼( LOG0
、 LOG1
、 LOG2
、 LOG3
、 LOG4
)都需要消耗 gas。它們具有的參數(主題)越多,它們消耗的 gas 就越多。

此外,像索引或數據大小等其他因素也會導致事件發出消耗更多 gas。
檢查 – 事件 – 交互模式
檢查-生效-交互模式 [9] 也適用於事件。
一種檢測這些模式的方法是使用 Remix 靜態分析工具。
這種模式也可以被 Slither 檢測到。當對一個在外部調用後觸發事件的合約運行 slither 時,你將得到一個發現,提示 「重入事件」。
因此,對於 dApp 來說,順序很重要,這樣你就可以正確地查看哪個事件首先、接下來和最後被發出。這在遞歸或重入調用的情況下尤其重要。如果在外部調用後觸發事件,並且這個外部調用進行了一個重入調用,那麼:
-
第一個發出的事件是第二次重入調用完成後的事件。
-
第二個發出的事件是初始交易後發出的事件。
理解這一點,也使得可以在鏈下提供清晰的審計跟蹤,以監視合約調用。你可以看到哪些函數首先和最後被調用,以及在執行交易期間每個例程的運行順序。
slither 檢測器文檔 [10] – Solidity 和 Vyper 的靜態分析器。
這種潛在的漏洞也在 Trail of Bits 對 Liquity [11] 智能合約的審計中發現並報告。


何時應該觸發事件?
在你的合約中可能有幾種情況下觸發事件可能很重要和有用。
-
當受限制的用戶和地址執行某些操作時(例如:所有者或合約管理員)。這包括例如受歡迎的
transfer ownership (address)
函數,該函數只能由所有者調用以更改合約的所有者。

-
更改一些關鍵變量或算術參數,這些變量負責合約的核心邏輯。在 DeFi 協議的背景下尤其重要。

Slither 檢測器文檔 [12] 中描述了更多關於這些情況的信息。
這也在 Trail 對 LooksRare 的審計報告中描述了。

-
監視在生產中部署的合約以檢測異常。

查看 0xprotocol [13] 的詳細信息,了解有關事件的安全相關問題。
參考
-
匿名事件使用目的的缺失文檔(知其所以然) [14]
-
[匿名事件的優勢]