---
# System prepended metadata

title: 2022-04-18 自己發 NFT—Part 2
tags: [' Cryptocurrency', Notes]

---

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

2022-04-18 自己發 NFT—Part 2
===
歡迎加入 Discord 聊天室「清交區塊鏈DAO」：https://discord.com/invite/rpQy37a8tk

這次讀書會延續上次分享的內容，錯過的朋友建議可以速速瀏覽一下
- 筆記：https://hackmd.io/9hFwrN0OSFepg0e6nceiSQ
- 錄影：https://www.youtube.com/watch?v=_yw_LybkHEA
- 若發現任何需要修正之處，請不吝前往 Discord 聊天室提出🙏

因為時間因素，所以上次的讀書會只提及簡易發行方法；藉著這次的讀書會，筆者將分享一些有趣的技巧，希望能帶改大家更多關於 NFT 賦能的靈感。

特別注意：如同發行 ERC20 token 只是創造 DeFi 產品的第零步，仍有太多未在此提及的 NFT 玩法可以探索，不應拘泥於本文提到的技巧；鼓勵大家持續學習新興 project 特點，也歡迎前往 Discord 聊天室與大家分享您的新發現。


Soulbound （NFT）
---
*Special thanks to Tommy Shen for the enlightenment.*

在前次讀書會的尾聲 Q&A 階段，觀眾 Tommy 提問一個相當棒的問題：
> 不好意思不知道剛剛有沒有提到~ soul binded NFT是要把transfer相關的那幾個function拿掉就可以了嗎

由於當時並沒有直接連想到 Tommy 意指 Buterin 大神在今年一月關於 Soulbound 概念的[文章](https://vitalik.ca/general/2022/01/26/soulbound.html)，因此給出不正確的回答，特此致歉並解說這篇文章的意涵。

---

首先，製作一款不能傳輸（transfer）的 NFT 合約，在技術上完全是可達成的。只需要鎖死[^1] `safeTransferFrom()` 和 `transferFrom()` 即可。然而，這跟「Soulbound」提出的觀點並不吻合。

Crypto asset 天生具備可轉移性，無論在合約當中怎麼限制其運作原理，外部使用者（i.e., 真實世界人類）一定可以使用終極手段做資產轉移 — 把 wallet 清空到只剩特定資產，然後賣掉 wallet 的私鑰。因此，在合約當中增加無謂的限制功能，只是減損 crypto asset 的流動性，有害而無益。

Buterin 在文中以魔獸世界（World of Warcraft）為例[^2]表達這個概念：

- 有沒有可能創造一種 NFT（或其他可透過合約實現的東西），讓持有這個「東西」的錢包，能夠彰顯它已經達成什麼事物或目標呢？

在魔獸世界當中，玩家破關或達成某些困難的任務，可以獲得各種不同的 soulbound；soulbound 不能當成道具買賣，也無法轉移、贈與給別的帳號，唯有透過達成任務才可獲得[^3]。

現實世界的許多有潛力以 NFT 型式呈現的物品也具有 soulbound 特性；例如：一個人持有駕照不代表他很有錢，~~也不代表他記得帶雞腿~~，只代表他完成了某種考試，並對他的「駕車技巧」在一定程度之上給予肯定；買賣駕照沒有任何實質的意義。

文中 [POAP](https://poap.xyz/) 認為是目前為止最能體現這種概念的實際案例，但是 Buterin 不覺得這就是所有問題的最終答案。另外，他還舉例了 [proof-of-humanity attestation](https://www.proofofhumanity.id/) 以及 [Kleros](https://kleros.io/) 也是值得參考的案例；Buterin 提議開發者可以把 NFT 與 ENS 做綁定納入考量範圍，不過就現階段來看，對 NFT 的傳輸門檻與 NFT 的安全性和方便性是一個取捨的問題。

另外，在思考 soulbound NFT 怎麼實現的過程中，仍然不能疏忽對於隱私性的重視！Buterin 認為對於那些在真實世界本可保持隱私的內容，不應該在轉換到 blockchain 世界後，就只能完全暴露給大家看。如果有一天每個人施打疫苗的紀錄只能以 POAP 的型式呈現，那會是一場可怕的災難。

在文章的最後，Buterin 這麼表示：
> If we can, this opens a much wider door to blockchains being at the center of ecosystems that are collaborative and fun, and not just about money.


[^1]: 方法不唯一，例如：規定只有 zero address 可以成功運行函數
[^2]: 抱歉筆者未玩過魔獸世界，所以可能對遊戲的運作機制存在不精確的敘述
[^3]: 當然，買賣魔獸世界的帳號即可「課金」取得各種 soulbound


`tokenURI()`
---
由於上次的筆記是使用 `ERC721URIStorage` 發行，因此這個函數就沒有解釋到。

以下 metadata extension 在 EIP-721 提案原文當中列為 optional [等級](https://datatracker.ietf.org/doc/html/rfc2119)

```solidity
interface ERC721Metadata {
    function name() external view returns (string _name);
    function symbol() external view returns (string _symbol);
    
    /**
     * @notice A distinct Uniform Resource Identifier (URI) for a given asset.
     * @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC
     * 3986. The URI may point to a JSON file that conforms to the "ERC721
     * Metadata JSON Schema".
     */
    function tokenURI(uint256 _tokenId) external view returns (string);
}
```

一般來說，`tokenURI()` 會寫成類似以下程式片段的函數：
```solidity
function tokenURI(uint256 tokenId) public view returns (string memory) {
    require(_exists(tokenId), "This token does not exist.");
    
    if(bytes(_baseURI).length > 0) {
        return string(abi.encodePacked(baseURI, tokenId.toString()));
    }
    else {
        return "";
    }
```

意思就是每個 NFT metadata 都是在單一固定的 `baseURI`（an IPFS directory CID） 後面加上「slash `tokenId`」；如同上次讀書會的個別 NFT 做設定的方法，反而較不常見。

舉例來說，把 `tokenId` 5233 塞給這個[合約](https://etherscan.io/address/0xa6916545a56f75acd43fb6a1527a73a41d2b4081#readContract)的 `tokenURI()` 函數，我們可以得到[^4]：
https://gateway.pinata.cloud/ipfs/QmSFS7KAmi2gsAA6najjDr7t4JbP6zHYue9QWMyMEBekbs/5233.json

這是打開 `baseURI` 的畫面節圖[^5]
<img src="https://i.imgur.com/rMR0kya.png" width="500"/>



[^4]: 事實上，傳輸到 IPFS 的檔案不必帶有附檔名。若當初上傳的資料夾只含「5233」這個名稱的檔案，則其完整的 URI 會變成去掉「.json」的網址，不過這不影響對於檔案的 JSON 解析。
[^5]: 我把 CID 轉成 v1，所以 IPFS 路徑看起來才有點不太一樣；CID-v1：bafybeib2dcmncbgmblvl5cbscqlrzpfq3uo66cghj3kquleu4dmswsy3pi


如果有機會，就不要用陣列！
---
Solidity 是一種奇妙的語言，許多地方的設計邏輯與以往的程式語言存在巨大差異。在以前，菜鳥工程師撰寫出低品質的程式，可能會造成系統被駭、程式執行效能低落等後果；然而，經過數十年的發展，security 領域在各種程式語言中已經建立不少實用的工具，減緩很多不經意造成的漏洞所致的傷害，而且在 web2.0 的世界要災難復原、倒回過去時間重新來過的難度與可能性，相對 blockchain 來說相當簡單[^6]。

另一方面，執行效能低落的程式對公司的傷害又更小，真是感謝摩爾定律[^7]；不過，這一切在 blockchain 世界[^8]就不是同一種故事了。所有的合約都會以同一種執行環境（EVM）、評比單位（gas）被大家檢視，一旦合約當中使用怪邏輯、爛設計，您就會看到 gas amount 變成天文數字，~~然後就被罵了~~。

以下附上一種範例，告訴各位讀者謹慎使用 array；如果可以，請盡量不要使用，否則會出現類似XX房的神奇事件[^9]。

https://gist.github.com/a2468834/d1495a43039c9225fd822e644e12867f?permalink_comment_id=4135495#gistcomment-4135495

[^6]: 此段之文意非指 security 議題不重要；筆者認為對於任何程式架構來說，永遠是安全至上
[^7]: ~~NVIDIA 說：再買更多卡！~~
[^8]: 至少對 EVM-compatiable chain 來說
[^9]: 包含但不限於謹此一例，筆者認為未來仍可能發生其他案例


ERC721Enumerable
---
EIP-721 的文件對於「列舉性」（enumeration）是這麼敘述：

> The enumeration extension is OPTIONAL for ERC-721 smart contracts (see “caveats”, below). This **allows your contract to publish its full list of NFTs** and make them discoverable.

```solidity
interface ERC721Enumerable {
    function totalSupply() external view returns (uint256);
    function tokenByIndex(uint256 _index) external view returns (uint256);
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}
```
- `totalSupply()`
    - 回傳整份 ERC721 合約的 NFT 總發行量
    - 原味 ERC721
        - 只能利用多次查詢「`_owners`」和「`_balances`」反推出 NFT 總發行量
    - ERC721Enumerable
        - 只有「合法的」NFT 才會被計入此數量，`tokenId` 的擁有者是 `address(0)`、燒掉的 NFT 等都不能算進來
- `tokenByIndex()`
    - 塞進某顆 NFT 的 `_index` 並回傳該 NFT 對應之 `tokenId`
    - 原味 ERC721
        - `tokenId` 可以不依連續順序、由小到大發行 NFT
    - ERC721Enumerable
        - 強制規定 `_index` **必須要**由小到大，而且不超過 `totalSupply()` 的回傳值
        - 此處的 `_index` 與 `tokenOfOwnerByIndex()` 所稱者不同
- `tokenOfOwnerByIndex()`
    - 塞進某個地址與 `_index` 並回傳該 NFT 對應之 `tokenId`
    - 原味 ERC721
        - 除非透過監聽 `Transfer()` 事件（計算速度很快且不花 gas），否則幾乎無法知道哪個地址持有哪些 `tokenId`
    - ERC721Enumerable
        - 強制規定 `_index` **必須要**由小到大，而且不超過 `balanceOf(_owner)` 的回傳值
        - 此處的 `_index` 與 `tokenByIndex()` 所稱者不同

<img src="https://i.imgur.com/0gUgh1H.jpg" width="650"/>


ERC721A
---
OpenZeppelin 的設計是為求最通用的（generic）使用場景，所以各合約採取切割非常乾淨的低耦合設計，搭配多層次繼承架構，創造出高組合性的合約範例庫。Azuki 提出的這個合約設計，是以一種特定的 NFT 使用情境去做設計（參見重要基本假設），因此方能寫出 gas efficiency 的架構，請讀者不要誤會 OpenZeppelin 有多麼地老舊不堪。

重要基本假設
- `tokenId` 只能**由小到大、單調遞增**地由 `_startTokenId()` 指定的數字開始鑄造（例如：0、1、2、3、4、5、、、）
- 任一個 NFT 持有者禁止持有超過 `type(uint64).max`（$2^{64}-1$）顆 NFT
- NFT 總發行量禁止超過 `type(uint256).max`（$2^{256}-1$）顆 NFT

改進的地方
1. Removing duplicate storage from OpenZeppelin’s ERC721Enumerable
    - <img src="https://i.imgur.com/EFygmji.jpg" width="550"/>
2. Updating the owner’s balance once per batch mint request, instead of per minted NFT
    - <img src="https://i.imgur.com/RwJLNWE.jpg" width="550"/>
3. Updating the owner data once per batch mint request, instead of per minted NFT
    - <img src="https://i.imgur.com/p6hWtuf.jpg" width="550"/>


Further reading
---
- EIP-721
    - https://eips.ethereum.org/EIPS/eip-721#rationale
- ERC721Enumerable
    - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/extensions/ERC721Enumerable.sol
- ERC721A
    - https://www.azuki.com/erc721a
    - https://github.com/chiru-labs/ERC721A
- A Guide to Designing Effective NFT Launches
    - https://www.paradigm.xyz/2021/10/a-guide-to-designing-effective-nft-launches
    - https://github.com/Anish-Agnihotri/MultiRaffle
- The Ultimate Guide to NFT Gas Optimization
    - https://medium.com/@WallStFam/the-ultimate-guide-to-nft-gas-optimization-7e9289e2d88f