---
title: "Yul 合約呼叫如何運作？"
description: "了解 Yul 智能合約程式語言中合約呼叫的運作方式"
---

# Yul 合約呼叫如何運作？

[Yul](http://www.alchemy.com/overviews/what-is-yul) 是一種中介程式語言,可用來在 [智能合約](https://www.alchemy.com/overviews/solidity-smart-contract) 中撰寫組合語言。現在你已經了解 Yul 的語法、儲存與記憶體運作方式,是時候來呼叫合約了!

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

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

<EmbeddedTable
  table={{
    columns: [
      { key: "1", width: 200, title: "Instruction", dataType: "object" },
      { key: "2", width: 200, title: "Explanation", dataType: "object" },
    ],
    data: [
      {
        "1": { title: "<p>gas()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>目前仍可用於執行的 gas 數量</p>", tooltip: "", icon: "" },
        id: 0,
      },
      {
        "1": { title: "<p>gasPrice()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>該交易的 gas price</p>", tooltip: "", icon: "" },
        id: 1,
      },
      {
        "1": { title: "<p>address()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>目前合約的位址</p>", tooltip: "", icon: "" },
        id: 2,
      },
      {
        "1": { title: "<p>balance(a)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>位址 a 的以太幣餘額,以 wei 為單位</p>", tooltip: "", icon: "" },
        id: 3,
      },
      {
        "1": { title: "<p>selfbalance()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>與呼叫 balance(address()) 相同,但成本稍低</p>", tooltip: "", icon: "" },
        id: 4,
      },
      {
        "1": { title: "<p>caller()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>呼叫該合約的位址（msg.sender）</p>", tooltip: "", icon: "" },
        id: 5,
      },
      {
        "1": { title: "<p>origin()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>發起該交易的位址（tx.origin）</p>", tooltip: "", icon: "" },
        id: 6,
      },
      {
        "1": { title: "<p>callvalue()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>該合約呼叫中傳送的以太幣數量，以 wei 為單位（msg.value）</p>", tooltip: "", icon: "" },
        id: 7,
      },
      {
        "1": { title: "<p>calldataload(p)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>從位置 p 開始的 call data（僅 32 位元組資料）</p>", tooltip: "", icon: "" },
        id: 8,
      },
      {
        "1": { title: "<p>calldatacopy(t, f, s)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>將 call data 複製到記憶體位置 t，從 calldata 中的位置 f 開始，複製 s 位元組的資料。</p>", tooltip: "", icon: "" },
        id: 9,
      },
      {
        "1": { title: "<p>extcodesize(a)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>位址 a 上程式碼的大小</p>", tooltip: "", icon: "" },
        id: 10,
      },
      {
        "1": { title: "<p>returndatasize()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>最後一次 returndata 的大小</p>", tooltip: "", icon: "" },
        id: 11,
      },
      {
        "1": { title: "<p>returndatacopy(t, f, s)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>從 return data 中位置 f 開始複製 s 位元組到記憶體位置 t</p>", tooltip: "", icon: "" },
        id: 12,
      },
      {
        "1": { title: "<p>timestamp()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>目前區塊的時間戳，以秒為單位</p>", tooltip: "", icon: "" },
        id: 13,
      },
      {
        "1": { title: "<p>number()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>目前區塊編號</p>", tooltip: "", icon: "" },
        id: 14,
      },
      {
        "1": { title: "<p>call(g, a, v, in, insize, out, outsize)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>呼叫位址 a 上的合約，使用 gas g，傳入 v wei 作為 msg.value，從記憶體位置 in - insize 傳入 tx.data，並將回傳資料儲存在記憶體位置 out - outsize。呼叫成功則回傳 1，否則回傳 0。</p>", tooltip: "", icon: "" },
        id: 15,
      },
      {
        "1": { title: "<p>delegatecall(g, a, in, insize, out, outsize)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>與 call() 類似。主要差異在於 delegatecall() 會更新呼叫端合約中的狀態變數。常用於代理合約（proxy contracts）。需要注意的是，儲存布局必須與你所呼叫的合約完全一致，以避免意外覆寫儲存變數。注意此處沒有參數 v。你無法透過 delegatecall() 傳送以太幣。</p>", tooltip: "", icon: "" },
        id: 16,
      },
      {
        "1": { title: "<p>staticcall(g, a, in, insize, out, outsize)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>與 call() 類似。差別在於它不能用來呼叫會改變區塊鏈狀態的合約（即標記為 pure 及 view 的函式除外）。注意此處沒有參數 v。你無法透過 staticcall() 傳送以太幣。</p>", tooltip: "", icon: "" },
        id: 17,
      },
    ],
  }}
/>

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

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

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

<EmbeddedTable
  table={{
    columns: [
      { key: "1", width: 200, title: "Memory Location", dataType: "object" },
      { key: "2", width: 200, title: "Value Stored", dataType: "object" },
    ],
    data: [
      {
        "1": { title: "<p>0x00</p>", tooltip: "", icon: "" },
        "2": { title: "<p>暫存空間（空）</p>", tooltip: "", icon: "" },
        id: 0,
      },
      {
        "1": { title: "<p>0x20</p>", tooltip: "", icon: "" },
        "2": { title: "<p>暫存空間（空）</p>", tooltip: "", icon: "" },
        id: 1,
      },
      {
        "1": { title: "<p>0x40</p>", tooltip: "", icon: "" },
        "2": { title: "<p>0x80（我們從未更新 Free Memory Pointer，因為 Yul 中所有操作都是手動完成的。）</p>", tooltip: "", icon: "" },
        id: 2,
      },
      {
        "1": { title: "<p>0x60</p>", tooltip: "", icon: "" },
        "2": { title: "<p>空</p>", tooltip: "", icon: "" },
        id: 3,
      },
      {
        "1": { title: "<p>0x80</p>", tooltip: "", icon: "" },
        "2": { title: "<p>0x0000000000000000000000000000000000000000000000000000000000773d45e0（函式選擇器）</p>", tooltip: "", icon: "" },
        id: 4,
      },
      {
        "1": { title: "<p>0xa0</p>", tooltip: "", icon: "" },
        "2": { title: "<p>0x0000000000000000000000000000000000000000000000000000000000000003（_var1）</p>", tooltip: "", icon: "" },
        id: 5,
      },
      {
        "1": { title: "<p>0xc0</p>", tooltip: "", icon: "" },
        "2": { title: "<p>0x0000000000000000000000000000000000000000000000000000000000000004（_var2）</p>", tooltip: "", icon: "" },
        id: 6,
      },
      {
        "1": { title: "<p>0xe0</p>", tooltip: "", icon: "" },
        "2": { title: "<p>空</p>", tooltip: "", icon: "" },
        id: 7,
      },
      {
        "1": { title: "<p>0x100</p>", tooltip: "", icon: "" },
        "2": { title: "<p>0x0000000000000000000000000000000000000000000000000000000000000003（第一個回傳值）</p>", tooltip: "", icon: "" },
        id: 8,
      },
      {
        "1": { title: "<p>0x120</p>", tooltip: "", icon: "" },
        "2": { title: "<p>0x0000000000000000000000000000000000000000000000000000000000000004（第二個回傳值）</p>", tooltip: "", icon: "" },
        id: 9,
      },
      {
        "1": { title: "<p>0x140</p>", tooltip: "", icon: "" },
        "2": { title: "<p>Free Memory Pointer（空）</p>", tooltip: "", icon: "" },
        id: 10,
      },
    ],
  }}
/>

### **如何在 Yul 中使用 delegate call**

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

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

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

這就結束了合約呼叫這個章節！接下來，來學習 [記憶體](http://www.alchemy.com/overviews/yul-memory)、[儲存](http://www.alchemy.com/overviews/yul-storage) 以及 [智能合約呼叫](http://www.alchemy.com/overviews/yul-contract-calls) 在 Yul 中是如何運作的。
