跳至內容
0%

Yul 合約呼叫如何運作?

發布於 2023年8月1日閱讀時間 4 分鐘

Yul 是一種中介程式語言,可用來在 智能合約 中撰寫組合語言。現在你已經了解 Yul 的語法、儲存與記憶體運作方式,是時候來呼叫合約了!

Yul 中合約呼叫是如何運作的?

在這系列的最後一個章節,我們要來看 Yul 中合約呼叫的運作方式。在深入範例之前,我們需要先學習更多 Yul 運算子。來看看吧。‍

Instruction
Explanation

gas()

目前仍可用於執行的 gas 數量

gasPrice()

該交易的 gas price

address()

目前合約的位址

balance(a)

位址 a 的以太幣餘額,以 wei 為單位

selfbalance()

與呼叫 balance(address()) 相同,但成本稍低

caller()

呼叫該合約的位址(msg.sender)

origin()

發起該交易的位址(tx.origin)

callvalue()

該合約呼叫中傳送的以太幣數量,以 wei 為單位(msg.value)

calldataload(p)

從位置 p 開始的 call data(僅 32 位元組資料)

calldatacopy(t, f, s)

將 call data 複製到記憶體位置 t,從 calldata 中的位置 f 開始,複製 s 位元組的資料。

extcodesize(a)

位址 a 上程式碼的大小

returndatasize()

最後一次 returndata 的大小

returndatacopy(t, f, s)

從 return data 中位置 f 開始複製 s 位元組到記憶體位置 t

timestamp()

目前區塊的時間戳,以秒為單位

number()

目前區塊編號

call(g, a, v, in, insize, out, outsize)

呼叫位址 a 上的合約,使用 gas g,傳入 v wei 作為 msg.value,從記憶體位置 in - insize 傳入 tx.data,並將回傳資料儲存在記憶體位置 out - outsize。呼叫成功則回傳 1,否則回傳 0。

delegatecall(g, a, in, insize, out, outsize)

與 call() 類似。主要差異在於 delegatecall() 會更新呼叫端合約中的狀態變數。常用於代理合約(proxy contracts)。需要注意的是,儲存布局必須與你所呼叫的合約完全一致,以避免意外覆寫儲存變數。注意此處沒有參數 v。你無法透過 delegatecall() 傳送以太幣。

staticcall(g, a, in, insize, out, outsize)

與 call() 類似。差別在於它不能用來呼叫會改變區塊鏈狀態的合約(即標記為 pure 及 view 的函式除外)。注意此處沒有參數 v。你無法透過 staticcall() 傳送以太幣。

好,現在讓我們來看看這些範例所需要的新合約。首先,來看看我們將要呼叫的合約。

這個合約有兩個儲存變數 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。

為了更清楚說明發生了什麼事,以下是我們回傳之前的記憶體布局:

Memory Location
Value Stored

0x00

暫存空間(空)

0x20

暫存空間(空)

0x40

0x80(我們從未更新 Free Memory Pointer,因為 Yul 中所有操作都是手動完成的。)

0x60

0x80

0x0000000000000000000000000000000000000000000000000000000000773d45e0(函式選擇器)

0xa0

0x0000000000000000000000000000000000000000000000000000000000000003(_var1)

0xc0

0x0000000000000000000000000000000000000000000000000000000000000004(_var2)

0xe0

0x100

0x0000000000000000000000000000000000000000000000000000000000000003(第一個回傳值)

0x120

0x0000000000000000000000000000000000000000000000000000000000000004(第二個回傳值)

0x140

Free Memory Pointer(空)

如何在 Yul 中使用 delegate call

在最後這一節,我們要看看 delegatecall()。程式碼會與 call() 幾乎一模一樣,只有一項改變。

我們做的唯一改變是把 call() 改成 delegatecall(),並移除 callvalue()。

我們不需要 callvalue(),因為 delegate call 是在自己的狀態下執行 CallMe 的程式碼。因此 a() 中的 require() 陳述式檢查的是是否有以太幣傳送到我們的 Caller 合約。如果我們查看 CallMe 中的 var1 和 var2,會發現沒有任何變化。然而,我們 Caller 合約中的 var1 和 var2 已成功更新。

這就結束了合約呼叫這個章節!接下來,來學習 記憶體儲存 以及 智能合約呼叫 在 Yul 中是如何運作的。

Background gradient

打造區塊鏈魔法

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