作者圖片
除了最近的地緣政治和宏觀經濟事件導致市場略微看跌外,我認為我們可以確信我們仍處於NFT 牛市的中間。
這個牛市導致每周啟動數百個項目,其中大部分是類似的智能合約。由於該領域的幾乎所有內容都是開源的,因此很容易實施被證明有效的解決方案。
然而,這導致項目複製粘貼了空間中流通的少數NFT 智能合約模板之一,而沒有真正了解每個實施中存在的不同優缺點。
為了稍微清理一下這個混亂,有必要回顧一下最流行的模板,檢查每個模板所做的權衡,並嘗試就不同類型項目的最佳合約得出一些結論。在為我當前的NFT/Metaverse項目MetaMorphies設計智能合約系統時,我還將擴展我的思維過程。
ERC721 和ERC721Enumerable
如果你不熟悉,NFT 源自EIP-721 Non-Fungible代幣標準提案。幾乎每個人都在使用的這個提議的OG 實現是由OpenZeppelin 完成的。
很快證明提供的功能ERC721是不夠的,許多項目開始採用ERC721Enumerable擴展。 ERC721此擴展通過在合約中添加所有代幣ID 的可枚舉性以及檢查帳戶擁有的所有代幣ID 的方法來增強原始功能。
然而,這就是我們開始遇到麻煩的地方。問題ERC721Enumerable是它做了很多不必要的事情,這將造幣廠功能的gas 成本推高到了天上,給社區造成了數百萬美元的損失。
ERC721Enumerable優化讀取功能以損害寫入功能。
ERC721Enumerable使用大量的冗餘存儲,這不僅增加了鑄造代幣的成本,而且還增加了轉移它們的成本。
這些是每次鑄幣或轉賬發生時執行的狀態修改函數。
我們可以清楚地看到ERC721Enumerable這是一個次優的選擇,不應再被大多數項目使用。
對我們來說幸運的是,一些智能開發人員已經註意到了這些低效率並設計了解決方案。
優化的ERC721Enumerable by Chance
當我偶然發現Chance的這篇很棒的文章時,我已經在考慮如何為上述問題建立一個適當的解決方案,這最終讓我的工作量減輕了很多。
建議大家閱讀整篇文章,熟悉解決方案,但是這個合約的基本前提是,它把優化的重心超越,把寫函數優化為讀函數的代價。這通常很好,因為如果讀取函數被稱為off-chain ,它們不會花費我們的錢。背後的基本原理ERC721Enumerable是提供對以下三個功能的訪問:totalSupply、tokenByIndex和tokenofOwnerByIndex。
我們可以忍受這些函數的低效率和無限循環,因為它們幾乎總是被稱為脫鏈。
此實現首先用單個數組替換_balances和_owners映射。 ERC721
優化的ERC721Enumerable by Chance
_mint函數變為:
偶然優化的ERC721
視圖功能ERC721Enumerable更改為:
是的,這個循環tokenOfOwnerByIndex確實效率低下,但這沒關係,因為它幾乎總是被稱為脫鏈,因此是免費的。
一個警告
如前所述,上述版本還刪除了_balances數組,因此修剪掉了額外的存儲寫入。結果,該_balanceOf函數循環遍歷整個_owners數組以確定地址的餘額。這又是一個非常低效的讀取操作,但是與相比tokenOfOwnerByIndex,我可以想像有幾十種情況需要在鏈上檢查地址的餘額。
例如,通過MetaMorphies,我們已經在開發NFT 質押礦池、代幣所有權授予投票權的治理系統以及移動增強現實應用程序。這個複雜系統的各個部分將檢查鏈上的餘額,因此它必須是高效的。
即使你不想掛載任何其他合約,如果你的鑄幣功能在允許鑄幣之前檢查地址擁有的代幣數量,你也應該小心。對於最初的幾個鑄幣者來說可能還好,但對於第8756 個不幸的鑄幣者來說,這將是一場災難。
你會從我這裡反复聽到這一點,但是當涉及到高價值操作時,盲目地複制粘貼代碼並不是要走的路。一個解決方案在作者的案例中完美運行的事實並不意味著它會自動適用於你項目的特殊需求。
代理和批准
Chance 建議的另一個優化是預先批准Open Sea 代理註冊合約來轉移你的代幣,並且還允許基礎合約所有者在未來將任何其他合約掛載到基礎合約,並true在isApprovedForAll.
該解決方案在多個方面存在嚴重問題:
如果Open Sea 代理註冊表受到威脅怎麼辦如果已安裝的合約受到損害怎麼辦如果基礎合約所有者決定拉扯並從合約中取出所有代幣怎麼辦?如果基礎合約所有者的錢包被盜怎麼辦
實施此模式後,如果發生上述任何情況,攻擊者可以從每個所有者那裡獲取所有NFT。它只會導致過多的攻擊向量和信任假設,節省20 美元的批准交易成本並不能證明可能損失數百萬美元的價值是合理的。
優化是一件非常好的事情,但有一些更優越的東西,這是區塊鍊和加密貨幣的基本前提:構建無信任系統。
ERC721A
另一個非常聰明且最近非常流行的解決方案是Chiru Labs 開發ERC721A的合約。讓我們繼續檢查這個智能合約內部發生了什麼。
的基本前提ERC721A是能夠以與鑄造單個NFT 幾乎相同的成本鑄造多個NFT。讓我們看看做了哪些優化,以及合約是如何工作的。
根據開發團隊的描述,沒有。 1 優化是刪除由引入的冗餘存儲ERC721Enumerable,類似於Chance 的做法。
沒有。 2和沒有。 3 項優化是每個批次鑄幣請求更新一次所有者的餘額和代幣所有權數據。
什麼優化沒有。 2和沒有。一旦你查看瞭如何從常規ERC721與ERC721A..
作者圖片
刪除了for 循環,從而ERC721A兌現了以與鑄造單個代幣相同的成本鑄造多個代幣的承諾。
但這引發了多個問題:合約如何存儲代幣ID 和所有權數據?如何確定代幣的所有權?轉移代幣如何運作?
ERC721A 存儲
ERC721A利用兩個結構和兩個映射來存儲所有權數據。
ERC721A 結構
從名字上看,它們的用途是不言而喻的。 TokenOwnership使用單個存儲槽來存儲有關令牌所有權的一些信息,並AddressData使用單個存儲槽來存儲有關鑄幣者地址的信息。
所採取的方法ERC721A起初可能看起來違反直覺,所以讓我們來看看在不同操作期間如何寫入和讀取存儲以達到合約的正確狀態。
假設我們是從合約中鑄造的第一個地址,我們鑄造了十個代幣。在這種情況下,會發生以下情況:
圖1:ERC721A 中的Mint 操作
在我們的批次中,第一個id 為0,因此合約AddressData使用批次大小和TokenOwnership該令牌id 和時間戳的結構配置結構。如你所見,其餘令牌ID 的數據為空。那我們如何確定所有權呢?這不是有問題嗎?
要理解為什麼不是,讓我們看看合約如何確定代幣的所有權。
圖2:在ERC721A 上調用ownerOf()
讓我們想像一下,這個操作是在上一個鑄造10 個代幣的操作之後進行的。我們對令牌id 3 的所有者感興趣,所以我們調用ownerOf(3). at 的槽_ownerships[3]是空的,所以函數移動到前一個id。它會這樣做,直到找到具有所有權地址的令牌。
好的,但是如果我將令牌id 0 轉移到另一個地址會發生什麼?我的所有代幣都會有空數據嗎?在這種情況下如何確定所有權?讓我們看看_transfer函數內部發生了什麼。
圖3:ERC721A 中的_transfer()
轉移代幣時,代碼檢查下一個代幣是否設置了所有者,如果沒有,則將from地址設置為所有者。我們知道代幣ID 是在鑄造時按升序分配的,因此如果一個地址鑄造了多個代幣,如果所有權數據未為其初始化,它也必須擁有下一個代幣。
在檢查合約時,我的第一個想法ERC721A是它確實使批量鑄幣廠的成本保持在較低水平,但它有一個討厭的循環,每次發生代幣轉移時_ownershipOf它都會調用。 _ownershipOf這似乎會回來並咬用戶的屁股。
ERC721A 中的_ownershipOf
這確實是一個合理的擔憂。為了說明這些成本會增加多少,讓我們想像一個不切實際的場景,我們在一次交易中鑄造350 個代幣,然後檢查代幣id 330 的所有權並將其轉移。 (該getOwner函數是一個簡單的函數,它調用ownerOf然後將某些內容寫入存儲,以說明任何寫入函數調用的成本ownerOf)。
ERC721A 的氣體估算
在2708 美元/ETH 和70 gwei/gas 的情況下,檢查所有權和轉移id 為330 的單個代幣的成本高於鑄造350 個代幣。這是因為_ 中的循環ownershipOf從330 變為0,並且每次SLOAD操作都要消耗gas。
如果我們再次鑄造350 個代幣,但轉移代幣id 1 而不是330,則數字看起來有很大不同。
ERC721A 的氣體估算
當然,我們不是Tubby Cats,所以我們會阻止任何人從我們的合約中鑄造這麼多代幣。假設我們將最大批量大小限制為10。如果有人鑄造10 個代幣,然後嘗試轉移id 為9 的代幣,數字如下所示:
ERC721A 的氣體估算
因此,根據你要轉移的代幣ID、檢查所有權,gas 成本可能會有很大差異。
對於許多考慮實施該合約的項目來說,這可能會破壞交易。一方面,如果你將批量大小限制為一個小數字,則可以節省大量資金並且是有效的。我認為大多數集合的批量大小都是有限的,比如說10,所以對於大多數人來說,這應該不是問題。
如果你不限制批量大小,則歸結為你是否相信你的客戶在轉移代幣之前會深入考慮智能合約,而不是讓你的項目誤以為單次代幣轉移會花費他們一大筆錢。
_ownershipOf如果你打算將此合約安裝到任何類型的複雜生態系統,你還必須考慮該功能的潛在缺陷。正如我在上面所說的,許多合約(例如質押礦池)檢查代幣的餘額和所有權,因此如果在鏈上調用這些功能,我們應該瞄準這些功能是高效的。
然而,根據開發人員的說法ERC721A,檢查所有權的成本“隨著代幣在集合中隨時間轉移而逐漸變為O(1)”。
成本比較
最後,讓我們進行一些測試,以直觀了解每種解決方案的潛在gas 成本。下面我們從每個實現中生成1、3、5 和10 個令牌5 次。 Custom721A是我們的ERC721A實現,CustomEnumerable是經過優化的ERC721Enumerable,OZEnumerable是Open Zeppelin 版本。
鑄造一些代幣的結果
這些觀察結果與我們上面討論的一致。 Open Zeppelin 版本在任何地方都非常昂貴且效率低下。如果你只鑄造一個代幣,則Custom721A和CustomEnumerable成本大致相同,如果你鑄造多個,則成本幾乎保持不變,Custom721A並隨著CustomEnumerable.
現在讓我們從上面嘗試我們的假設場景:我們鑄造350 個代幣,然後隨機選擇20 個代幣ID,然後檢查所有權並轉移它們(已知的弱點ERC721A)。
鑄造和轉移代幣的結果
這也與我們上面討論的一致。檢查所有權的成本大約是的5 倍ERC721A,而轉移代幣的成本大約是4 倍。
我們還可以清楚地看到,批量鑄幣是ERC721A真正閃耀的地方。鑄造350 個代幣只需147 美元,而CustomEnumerable同樣的操作成本要高出11 倍。但同樣,一批鑄造350 個代幣是非常不現實的。
結論— 一個ERC721 來統治所有這些?
所以不久,你是否正處於十字路口,正在尋找我的答案?使用哪個實現?如果你做到了這一步,我有義務給你一個明確的答案,對吧?好吧,我可以非常自信地說這取決於。
本文最重要的一點應該是,當涉及到智能合約時,你必須了解導入代碼庫的所有內容的來龍去脈。請不要盲目地複制粘貼代碼,即使它來自非常可靠的來源,因為它們的解決方案可能不適合你正在處理的項目的特定需求。
感謝你花時間閱讀本文,祝你編碼愉快,並祝你好運