Yul 合約呼叫如何運作?
Yul 是一種中介程式語言,可用來在 智能合約 中撰寫組合語言。現在你已經了解 Yul 的語法、儲存與記憶體運作方式,是時候來呼叫合約了!
Yul 中合約呼叫是如何運作的?
在這系列的最後一個章節,我們要來看 Yul 中合約呼叫的運作方式。在深入範例之前,我們需要先學習更多 Yul 運算子。來看看吧。
好,現在讓我們來看看這些範例所需要的新合約。首先,來看看我們將要呼叫的合約。
這個合約有兩個儲存變數 var1 和 var2,分別儲存在儲存槽(storage slot)1 和 2 中。
函式 a() 要求使用者至少向合約傳送 1 個以太幣,否則會 revert。
接著,函式 a() 會更新 var1 和 var2 並回傳它們。
函式 b() 只是讀取 var1 和 var2 並回傳它們。
在進入呼叫 CallMe 合約的合約之前,我們需要先花點時間了解函式選擇器(function selector)。
來看看以下這筆交易的 call data 0x773d45e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002。
calldata 的前 4 個位元組就是所謂的函式選擇器(0x773d45e0)。這是 EVM 用來得知你想呼叫哪個函式的方式。我們透過對函式簽章字串取雜湊,取其前 4 個位元組來得出函式選擇器。
因此函式 a() 的簽章會是 a(uint256,uint256)。
對這個字串取雜湊後得到:0x773d45e097aa76a22159880d254a5f1db8365bc2d0f0987a82bda7dfd3b9c8aa。
觀察前 4 個位元組,可以看到等於 0x773d45e0。
注意簽章中沒有空格。這點很重要,因為加上空格會得到完全不同的雜湊值。你不需要擔心如何取得我們範例程式碼的選擇器,我會直接提供給你。
讓我們先來看看儲存布局(storage layout)。
注意 var1 和 var2 與 CallMe 合約的布局相同。你可能還記得我提過,為了讓 delegatecall() 正常運作,布局必須與另一個合約相同。
我們滿足了這個需求,同時也能擁有其他變數(selectorA 和 selectorB),只要新變數是附加在後面即可。這樣可以避免任何儲存衝突(storage collisions)。
現在我們可以進行第一次合約呼叫了。
如何在 Yul 中使用 staticcall()
先從簡單的開始,staticcall()。以下是我們的函式:
這裡發生了什麼事:
- 透過載入儲存槽 2 從儲存中取得 b() 的函式選擇器(兩個選擇器都封裝在同一個儲存槽中)
- 向右移位 4 個位元組(32 個位元)以取出 selectorB
- 將函式選擇器儲存到記憶體的暫存空間(scratch space)
- 進行我們的 static call
- 傳入 gas()(你也可以指定 gas 的數量)
- 傳入參數 _callMe 作為合約位址
- 檢查函式呼叫是否成功,否則我們會不帶資料直接回傳
- 從記憶體回傳資料,並看到數值 1 和 2
注意: 0x1c 和 0x20 代表我們只想傳入所儲存內容最後 4 個位元組到暫存空間。最後兩個 staticcall() 參數指定我們要將回傳資料儲存在記憶體位置 0x80 - 0xc0。
如何在 Yul 中使用 call()
接下來看看 call()。我們要呼叫 CallMe 中的函式 a()。記得要向合約傳送至少 1 個以太幣!在這個範例中,我會傳入 3 和 4 作為 _var1 和 _var2。
以下是程式碼:
好,與上一個範例類似,我們需要載入 slot2。不過這次我們要遮罩(mask)selectorB 以取出 selectorA。
現在我們要將選擇器儲存在 0x80。
由於我們需要 calldata 中的參數,我們要使用 calldatacopy()。我們告訴 calldatacopy():
- 將我們的 calldata 儲存在記憶體位置 0xa0
- 跳過前 36 個位元組
- 儲存 calldata 大小減去 36 位元組的資料
注意: 我們跳過的前 4 個位元組是 callA() 的函式選擇器,接下來的 32 個位元組是 callMe 的位址。
現在我們可以進行合約呼叫了!
跟上次一樣,我們傳入 gas() 和 _callMe。不過這次我們從 0x9c(0x80 記憶體序列的最後 4 個位元組)到 0xe0 傳入我們的 call data,並將資料儲存在記憶體位置 0x100 - 0x120。
同樣地,我們檢查呼叫是否成功並回傳輸出。如果我們查看 CallMe 合約,會看到數值已成功更新為 3 和 4。
為了更清楚說明發生了什麼事,以下是我們回傳之前的記憶體布局:
如何在 Yul 中使用 delegate call
在最後這一節,我們要看看 delegatecall()。程式碼會與 call() 幾乎一模一樣,只有一項改變。
我們做的唯一改變是把 call() 改成 delegatecall(),並移除 callvalue()。
我們不需要 callvalue(),因為 delegate call 是在自己的狀態下執行 CallMe 的程式碼。因此 a() 中的 require() 陳述式檢查的是是否有以太幣傳送到我們的 Caller 合約。如果我們查看 CallMe 中的 var1 和 var2,會發現沒有任何變化。然而,我們 Caller 合約中的 var1 和 var2 已成功更新。
相關總覽

打造區塊鏈魔法
Alchemy 結合最強大的 Web3 開發者產品與工具,並提供資源、社群與卓越的支援。


