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