深入了解Solidity事件—Event

來源:登鏈社區

在今天的文章中,我們將看一下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 無名氏 {
event SecretPasswordHashUpdated(bytes32 secretPasswordHash) anonymous;
}

如果事件宣告為anonymous,在合約ABI 中,事件的”anonymous”欄位將標記為true。

img https://github.com/ethereum/solidity/issues/13086

匿名事件的一個優點是,它使你的合約更便宜部署,並且在觸發時Gas方面也更便宜。

匿名事件的一個很好的用例是對於只有一個事件的合約。監聽合約中的所有事件是有意義的,因為只有這一個事件會出現在事件日誌中。訂閱其名稱是無關緊要的,因為只定義了一個單一事件來由合約發出。因此,你可以將事件定義為匿名,並訂閱來自合約的所有事件日誌,並確認它們都是相同的事件。

查看匿名事件在流行程式碼庫中的使用範例,如在DappHub 的DS-Note 合約[7] 中。

img 來源程式碼[8]

我們可以在上面的程式碼片段中看到,由於事件宣告為匿名,這使得定義第四個「indexed」參數。

請注意,由於匿名事件沒有bytes32 主題哈希,因此匿名事件不支援.selector 成員。

使用LOG 操作碼在組譯中觸發事件

img https://docs.soliditylang.org/en/v0.8.19/yul.html#evm-dialect

在組譯中觸發事件是可能的,使用logN 指令,該指令對應於EVM 指令集中的操作碼。

要在彙編中觸發事件,你必須將要由事件發出的所有資料儲存在memory 中的特定位置。

一旦你將要由事件發出的資料儲存在記憶體中,然後可以將以下參數指定給logN 指令:

  • p = 從中開始取得資料的記憶體位置。基本上這是一個記憶體指針,或者是一個“偏移量”或“記憶體索引”,這取決於你如何稱呼它。

  • s = 你希望從p 開始在事件中發出的位元組數。

  • 所有其他參數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
)
}
}

事件的gas 成本

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

image-20240226195203141

此外,像索引或資料大小等其他因素也會導致事件發出消耗更多gas。

檢查- 事件- 互動模式

檢查-生效-互動模式[9]也適用於事件。

一種偵測這些模式的方法是使用Remix 靜態分析工具。

這種模式也可以被Slither 偵測到。當對一個在外部呼叫後觸發事件的合約執行slither 時,你將得到一個發現,提示「重入事件」。

因此,對於dApp 來說,順序很重要,這樣你就可以正確地查看哪個事件首先、接下來和最後被發出。這在遞歸或重入呼叫的情況下尤其重要。如果在外部調用後觸發事件,並且這個外部調用進行了一個重入調用,那麼:

  1. 第一個發出的事件是第二次重入呼叫完成後的事件。

  2. 第二個發出的事件是初始交易後發出的事件。

理解這一點,也使得可以在鏈下提供清晰的審計跟踪,以監視合約調用。你可以看到哪些函數首先和最後被調用,以及在執行交易期間每個例程的運行順序。

slither 偵測器文檔[10] – Solidity 和Vyper 的靜態分析器。

這種潛在的漏洞也在Trail of Bits 對Liquity[11] 智能合約的審計中發現並報告。

imgimg

何時應該觸發事件?

在你的合約中可能有幾種情況下觸發事件可能很重要且有用。

  • 當受限制的使用者和地址執行某些操作時(例如:所有者或合約管理員)。這包括例如受歡迎的transfer ownership (address) 函數,該函數只能由所有者呼叫以更改合約的所有者。

img

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

img

Slither 偵測器文檔[12]中描述了更多關於這些情況的資訊。

這也在Trail 對LooksRare 的審計報告中描述了。

img

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

img

查看0xprotocol[13] 的詳細信息,了解有關事件的安全相關問題。

參考

  1. 匿名事件使用目的的缺失文件(知其所以然)[14]

  2. [匿名事件的优势]

Total
0
Shares
Related Posts