---
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