---
# System prepended metadata

title: 關於「smart contract 的函數回傳值」
tags: [' Cryptocurrency', Notes]

---

---
lang: zh-tw
tags: Notes, Cryptocurrency
date: 20220319
robots: noindex, nofollow
license: GPL-3.0
---

關於「smart contract 的函數回傳值」
===
Introduction
---
建構 smart contract 的過程不只有實作合約函數內的邏輯，很多時候我們會希望透過呼叫一系列的函數，並在結束運行時回傳一個數值（return value），讓我們的其他程式能夠使用該值來接續整個工作流程[^1]。

假設我們寫了一個這樣的合約，那麼我們要怎麼做才能獲得 `get()` 和 `set()` 的回傳值呢？
```solidity=0.8.1
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.1;

contract Foo {
    uint256 public bar;
    
    function set(uint256 _value) public returns (uint256) {
        bar += _value;
        return bar;
    }
    
    function get() public view returns (uint256) {
        return bar;
    }
}
```


Read-Only Method
---
由於 `get()` 函數是 view function，所以它不會造成合約的狀態（state）有任何改變；因此，我們能夠在不發出 transaction （txn）的情況下[^2]，就促使 EVM 執行這個函數並取得相對應的回傳值。

使用以下 JavaScript 腳本，我們可以透過呼叫 `get()` 來直接印出 `bar` 數值。
```javascript
async function main() {
    /* Assume that contract Foo has been deployed. */
    const contract = /* Ethers.js Contract object */;
    const signer   = /* Ethers.js Signer object */;
    var txn;
    
    contract = contract.connect(signer);
    txn      = await contract.get(); // Expected return: 0
    
    console.log(JSON.stringify(txn, null, 2));
}
```
在 terminal 印出數值為零[^3]的 BigNumber[^4] 物件
```shell
{
  "type": "BigNumber",
  "hex": "0x00"
}
```
真是相當地的輕鬆寫意，就如同一般 javascript 套件流程那樣呼叫函數、取得回傳值；然而，呼叫 `set()` 就沒有這麼容易了。


Write Method
---
熟悉 solidity 的讀者可以知道 `set()` 函數沒辦法加上 view 關鍵字，因為它會修改 state variable `bar` 數值，進而更改 contract state。對於這類的函數，我們沒辦法在不發出 txn 的情況下[^5]，就完成 `set()` 函數的呼叫；不過 Ethers.js 並不是未提供發交易的 API，因此到目前為止仍然沒有特別困難。

使用以下 JavaScript 腳本，我們看看透過發出 txn 來呼叫 `set()` 會得到什麼東西呢？
```javascript
async function main() {
    /* Assume that contract Foo has been deployed. */
    const contract = /* Ethers.js Contract object */;
    const signer   = /* Ethers.js Signer object */;
    var txn;
    
    contract = contract.connect(signer);
    txn = await contract.set(5); // Expected return: 5
    txn = await txn.wait();
    
    console.log(JSON.stringify(txn, null, 2));
}
```
以下是在 terminal 可以看到的東西
```shell
{
  "to": "0xD014...",
  "from": "0xf39F...",
  "contractAddress": null,
  "transactionIndex": 0,
  "gasUsed": {...},
  "logsBloom": "0x0000...",
  "blockHash": "0xa84e...",
  "transactionHash": "0xcb7e...",
  "logs": [],
  "blockNumber": 1391...,
  "confirmations": 1,
  "cumulativeGasUsed": {...},
  "effectiveGasPrice": {...},
  "status": 1,
  "type": 2,
  "byzantium": true,
  "events": []
}
```
顯然印出的東西當中[^6]沒有發現任何和「5」有關的 property 存在，但是這並非 Ethers.js 套件的問題，也不是使用錯誤的 API 所導致。


為什麼？
---
首先，我們可以看看 Ethers.js 的說明[文件](https://docs.ethers.io/v5/api/contract/contract/#Contract--write)怎麼敘述這件事情。

> Write Methods (non-constant)
> 
> A non-constant method requires a transaction to be signed...
> ...
> **It cannot return a result.** If a result is required, it should be logged using a Solidity event (or EVM log), which can then be queried from the transaction receipt.

說明文件直白地告訴我們呼叫 write method 就是不能獲得回傳值，這意味著本文開頭「想要取得回傳值」的這個想法，似乎不適用在 blockchain 開發環境當中。另外，我們也可以從 yellow paper[^7] 當中找到同樣的說法。

> In the case of executing a message call,...
> ...
> Aside from evaluating to...
> ...
> **This is ignored when executing transactions**, however message calls can be initiated due to VM-code execution and in this case this information is used.

這段文字清楚的敘述到我們看到的現象：已被礦工驗證完成且納入 block 當中的 txn 不會攜帶關於 write method 的回傳值，因為它被直接丟掉了；但是，合約之間的互相呼叫（message call）則會保留回傳值。

由於 blockchain 世界極為 asynchronous，此點可由 chain reorganisation 等現象輕易得知，因此開發者不應該預期發出 txn 之後，可以在固定的時間內（甚至無法預期時間長短）獲得 txn 已被納入 block 的訊息；除此之外，txn 也可能因為種種因素[^8]，得到 reverted txn 或未被執行過，這樣的情況更不可能獲得回傳值。

基於 blockchain 這樣的特性，我們應該使用別種方式（就是 event）來記錄、取得函數運行過程當中，有興趣的關鍵變數的狀態。

使用 Event
---
在 solidity 說明[文件](https://docs.soliditylang.org/en/v0.8.13/structure-of-a-contract.html#events)當中是這麼敘述 event：

> Events are convenience interfaces with the EVM logging facilities.

就像我們經常在 python 腳本使用 `print()` 或 `logging.info()`，在 javascript 腳本使用 `console.log()`，如果開發者希望在 contract 的執行過程中，EVM 可以給出一些 log 文字，那當然非使用 `event` 莫屬。特別注意： `event` 噴出的文字不能被 contract 在稍後存取使用，它只能由外部世界（Ethereum 以外的真實世界）的人爬梳、查詢。

應用這樣子的概念，我們把一開始的 `Foo` contract 作一點小小的修改
```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.1;

contract FooV2 {
    uint256 public bar;
    
    event ChangeValue(uint256 new_bar);
    
    function set(uint256 _value) public returns (uint256) {
        bar += _value;
        emit ChangeValue(bar);
        return bar;
    }
    
    function get() public view returns (uint256) {
        return bar;
    }
}
```
同樣地，使用以下 JavaScript 腳本，我們看看這次能不能找到想要的數值呢？
```javascript
async function main() {
    /* Assume that contract Foo has been deployed. */
    const contract = /* Ethers.js Contract object */;
    const signer   = /* Ethers.js Signer object */;
    var txn;
    
    contract = contract.connect(signer);
    txn = await contract.set(5); // Expected return: 5
    txn = await txn.wait();
    
    console.log(JSON.stringify(txn, null, 2));
}
```
以下是在 terminal 可以看到的東西[^10]
```shell
{
  "to": "0xD014...",
  "from": "0xf39F...",
  "contractAddress": null,
  "transactionIndex": 0,
  "gasUsed": {...},
  "logsBloom": "0x0000...",
  "blockHash": ""0x25b0...",
  "transactionHash": "0xb242...",
  "logs": [
    {
      "transactionIndex": 0,
      "blockNumber": 13914058,
      "transactionHash": "0xb2420...",
      "address": "0xD014...",
      "topics": [
        "0x817b1c06cdeffca4116d87d0b3b928661475824ccecc5d542300e4f06d89f253"
      ],
      "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
      "logIndex": 0,
      "blockHash": "0x25b0..."
    }
  ],
  "blockNumber": 1391...,
  "confirmations": 1,
  "cumulativeGasUsed": {...},
  "effectiveGasPrice": {...},
  "status": 1,
  "type": 2,
  "byzantium": true,
  "events": [
    {
      "transactionIndex": 0,
      "blockNumber": 1391...,
      "transactionHash": "0xb2420...",
      "address": "0xD014...",
      "topics": [
        "0x817b1c06cdeffca4116d87d0b3b928661475824ccecc5d542300e4f06d89f253"
      ],
      "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
      "logIndex": 0,
      "blockHash": "0x25b0...",
      "args": [
        {
          "type": "BigNumber",
          "hex": "0x05"
        }
      ],
      "event": "ChangeValue",
      "eventSignature": "ChangeValue(uint256)"
    }
  ]
}
```
哇啊，看起來比之前豐富許多！尤其在 `events.args` attribute 當中可以看到我們最想要得到的更新後數值！[^11]。可能會有讀者好奇 `topics` 是什麼東西？這個詞就與 `indexed` 這個關鍵字有關聯了。

`topics`編號 0 （或使用 array 的觀點就是 index 0）的東西，永遠是 event signature 的 hash value。以這邊的例子來說，就是把「`ChangeValue(uint256)`」這串文字拿去給 keccak256 計算。

0. 假設 event 長這樣：`event ChangeValue(uint256 new_bar);`
1. 我們把 solidity code 裡面 event 的關鍵字、變數名稱都去掉
    - 得到 `ChangeValue(uint256)`
2. 把得到的東西轉成 UTF-8 byte array
3. 丟給 keccak256 作雜湊計算

以下是使用 Ethers.js API 獲得 event signature 的方法[^14]
```javascript
var name            = ethers.utils.toUtf8Bytes("ChangeValue(uint256)");
var event_signature = ethers.utils.keccak256(name);
console.log(event_signature);
```

`topics`編號 1~3 （或使用 array 的觀點就是 index 1~3）的東西，則是有添加 `indexed` 關鍵字的 arguments。`indexed` 最多只能有三個，但是 `event` 可以容納的總 arguments 則無限制。有添加 `indexed` 關鍵字的 arguments 會包含在 `topics` attribute 當中，沒有的則會通通包在 `events.data` attribute 當中，可能需要自行解碼（不過 Ethers.js 會盡量把能解碼的東西都解碼）。

特別注意：添加 `indexed` 關鍵字的 input argument 們，每個限制長度為 256 bits，**超過會被使用 keccak256 強制縮短**[^13]；因此，`string` 型別的關鍵字應視情況[^12]避免加上 `indexed` 關鍵字。

假設我們把原本的 `event` 改寫成這樣
```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.1;

contract FooV2 {
    uint256 public bar;
    
    event ChangeValue(uint256 indexed new_bar, string indexed log1, string log2);
    
    function set(uint256 _value) public returns (uint256) {
        bar += _value;
        emit ChangeValue(bar, "Hello World!", "Hello World!");
        return bar;
    }
    
    function get() public view returns (uint256) {
        return bar;
    }
}
```
以下是在 terminal 可以看到的東西，大致上跟之前差不多
```shell
{
...
  "events": [
    {
      "transactionIndex": 0,
      "blockNumber": 1391...,
      "transactionHash": "0x6d4a...",
      "address": "0xD014..",
      "topics": [
        "0xaaef25bbee5e5fdfd20f17f7cfeeb90a41a514ef8b062ec99d35891429e7bdb6",
        "0x0000000000000000000000000000000000000000000000000000000000000005",
        "0x3ea2f1d0abf3fc66cf29eebb70cbd4e7fe762ef8a09bcc06c8edf641230afec0"
      ],
      "data": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000",
      "logIndex": 0,
      "blockHash": "0x40b1...",
      "args": [
        {
          "type": "BigNumber",
          "hex": "0x05"
        },
        {
          "_isIndexed": true,
          "hash": "0x3ea2f1d0abf3fc66cf29eebb70cbd4e7fe762ef8a09bcc06c8edf641230afec0"
        },
        "Hello World!"
      ],
      "event": "ChangeValue",
      "eventSignature": "ChangeValue(uint256,string,string)"
    }
  ]
}
```
我們可以發現 `"topics"` attribute 多了兩行東西：被添加 `indexed` 關鍵字的兩個 input arguments `new_bar` 和 `log1`。另外，我們也可以發現同樣都是塞 `Hello World!` 字串，但是有加 `indexed` 關鍵字 `log1` 只剩一串 256-bit hash value；反觀，另一個 `log2` 則能被解碼回 `Hello World!` 字串。

最後，event log 的功能不只是拿來給開發者看看 contract 運行過程當中發生了什麼事，它能拿來「追蹤」特定事件的發生，例如：追蹤特定 ERC-20 token 的 `Transfer()` 事件。如此一來再也不必針對所有新出現的 txn 一個一個剖析是否有我們感興趣的 ERC-20 token 被轉移，只需要尋找特定 event log 就好。

後續補充小筆記：https://discord.com/channels/938615061549842432/948252983492218880/1000058476401270784


查詢特定合約的 Event
---
承接前述內容，如果我們今天想要搜尋特定一個合約的特定一些 event，那要怎麼辦呢？

假設我們對 WrappedEther 合約特定幾個地址的轉帳行為感到有興趣，則只需要使用以下方法，就能針對特定 block number 區段撈取符合指定條件的 event。

```javascript
async function main() {
    /* Assume that contract Foo has been deployed. */
    const contract = /* Ethers.js Contract object */;
    
    const from     = 14680707;
    const to       = 14687734;
    const address0 = "0xa69606e95a3358907bf861fd1602f3c49a267987"; // Randomly choosing
    const address1 = "0x1d88001a3661c7e2b886deceabdf67e4c8d89d4b"; // Randomly choosing
    const filter   = {
        address: null,
        topics: [
            [ // OR-Conditions of "Topics[0]"
                ethers.utils.id("Transfer(address,address,uint256)")
            ],
            [ // OR-Conditions of "Topics[1]"
                ethers.utils.hexZeroPad(address0, 32),
                ethers.utils.hexZeroPad(address1, 32)
            ]
        ]
    };
    
    var results = await contract.queryFilter(filter, from, to);
    console.log(JSON.stringify(results, null, 2));
}
```
以下是在 terminal 可以看到的東西
```shell
[
  {
    "blockNumber": 14680707,
    "blockHash": "0x8ff1...",
    "transactionIndex": 132,
    "removed": false,
    "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
    "data": "0x0000000000000000000000000000000000000000000000000494654067e10000",
    "topics": [
      "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
      "0x0000000000000000000000001d88001a3661c7e2b886deceabdf67e4c8d89d4b",
      "0x000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
    ],
    "transactionHash": "0xf7e1...",
    "logIndex": 195
  },
  {
    "blockNumber": 14687289,
    ...
  },
  {
    "blockNumber": 14687734,
    ...
  }
]
```
各位讀者需要特別注意到以下幾點
1. 查詢特定 event 的過濾條件 `filter` 變數，只有 `address` 和 `topics` 兩個屬性；換句話說，在原本 solidity code 裡面，**未使用** `indexed` 關鍵字標註的參數皆**沒辦法**當成過濾條件。
2. 有些 Ethereum 節點供應商會限制查找的範圍或符合條件的結果總量，因此盡量以精準定義「過濾條件」為佳；當然，自架節點則無此限制。
3. `ethers.utils.id("")` 是用來求解某字串 KECCAK256 hash value 的[函數](https://docs.ethers.io/v5/api/utils/hashing/#utils-id)，也可以使用 `ethers.utils.keccak256(ethers.utils.toUtf8Bytes(""))` 來表達，當然直接塞 "0xddf252..." 進去也是行的通。
4. 透過調整 `filter` 變數的 `topics` 屬性，可以得到不同結果；最外層陣列代表 Topics[0]、Topics[1]、Topics[2]、Topics[3] 過濾條件，次一層的陣列則是以「OR」運算串聯在一起的過濾條件，詳細可見這邊[說明](https://docs.ethers.io/v5/concepts/events/#events--filters)。
5. 同樣的 `filter` 變數也可以塞給 Ethers.js provider events [API](https://docs.ethers.io/v5/api/providers/provider/#Provider--event-methods)。


附錄：`callStatic`
---
雖然上面的介紹已經給出 yellow paper 的說明，原則上就是代表不能那樣子操做 write method，不過 Ethers.js 仍有提供一個「近似」的 API[^9] 給我們使用。

> *contract* . *callStatic* . **METHOD_NAME**( ...args [ , overrides ] ) ⇒ *Promise< any >*
> 
> Rather than executing the state-change of a transaction, **it is possible to ask a node to pretend** that a call is not state-changing and return the result.
> 
> **This does not actually change any state**, but is free. This in some cases can be used to determine if a transaction will fail or succeed.

類似 `contract.populateTransaction.METHOD_NAME()` 的概念，在 Ethers.js 當中使用此 API 的確可以獲得如同呼叫 read-only method 時的輕鬆寫意，但是這並不會發送任何 txn 給礦工；因此，只能稱得上「模擬」該筆 txn 的執行結果，相當可能隨著時間的遞移、Ethereum world state 的轉換，在給出相同 input arguments 卻獲得不同的結果。
```javascript
async function main() {
    /* Assume that contract Foo has been deployed. */
    const contract = /* Ethers.js Contract object */;
    const signer   = /* Ethers.js Signer object */;
    var txn;
    
    contract = contract.connect(signer);
    txn = await contract.callStatic.set(5); // Expected return: 5
    
    console.log(JSON.stringify(txn, null, 2));
}
```
以下是在 terminal 可以看到的東西
```shell
{
  "type": "BigNumber",
  "hex": "0x05"
}
```
另外，也許有讀者好奇 Remix 是怎麼做到 decoded output 功能的呢？由於筆者尚未釐清開發團隊的實作方法，因此無法詳述給讀者知道；然而，透過找到 yellow paper 當中的相關文字敘述，就有高度可能性亦為透過類似 `callStatic` 的機制實作出來；因此，筆者建議需要開發複雜合約的讀者，能夠善用熱門框架來進行開發，而非只使用 Remix 卻忽略了許多必須熟悉透徹的執行細節。

以下是 Remix txn 截圖（部屬相同合約並選擇 JavaScript VM (London)）
<img src="https://hackmd.io/_uploads/rkTld8HE3.png" width="700"/>


[^1]: 筆者使用 Ethers.js 套件，至於習慣用 web3.js 的讀者，應可從網路上搜尋到對應語法的文章
[^2]: Ethers.js 文件當中稱為 read-only method 或 constant method
[^3]: Solidity 預設所有變數的初始化數值都是 binary representation 的零
[^4]: https://docs.ethers.io/v5/api/utils/bignumber/#BigNumber
[^5]: Ethers.js 文件當中稱為 write method 或 non-constant method
[^6]: 有省略部分欄位的數值
[^7]: https://ethereum.github.io/yellowpaper/paper.pdf 的「8. Message Call」段落
[^8]: 例如：gas price 太低、gas limit 給不夠、txn 包含 invalid message、使用重複 nonce 發出 txn、礦工的 mempool 遺失你的 txn 但你沒有重發送、、、
[^9]: https://docs.ethers.io/v5/api/contract/contract/#contract-callStatic
[^10]: 有省略部分欄位的數值
[^11]: 眼尖的讀者可能會發現 `logs` attribute 也可以發現相同的東西，不過那是原始 txn 裡面尚未解碼的 event log；當然可以自行透過簡單的方法去解碼它，不過 Ethers.js 已經幫我們把解碼完的東西都包在 `events` 裡面囉，就不用這麼麻煩了
[^12]: Event log 越長所需的 gas 越貴，雖然 gas 不會到指數成長的那種可怕貴，不過以能省則省的角度來思考，仍然是盡量避免把長篇累牘的 `string` 塞進 event log
[^13]: https://docs.soliditylang.org/en/v0.8.13/abi-spec.html#encoding-of-indexed-event-parameters
[^14]: 求得 function selector 的方法也一樣，差別在 function selector 只取 keccak256 吐出的結果的前 4 bytes (32 bits)；在 solidity 的語法則是 `<contract_instance>.<function_name>.selector` 