全面理解ERC721的实现机制
TL; DR
基本上,由于ERC721的所有權基于唯一索引或ID的所有權,因此需要將令牌創建和傳輸的基本原理外推以適應這種情況。 此外,最新的完整實現還包括一個safeTransferFrom()函數,用于在傳輸令牌之前檢查標準接口的實現。ERC721令牌
圍繞對ERC721的興趣,我已經看到了許多關于可以保存元數據的非可替換標記的材料,但是我發現的材料深度讓我尋找更多細節。 我對ERC721的興趣始于2月份的EthDenver--您可以在這里閱讀關于我們使用ERC721的項目。 此后,我對ERC721標準的實施進行了更新,因為我看到由此設計帶來的更多價值。 作為本周從Open Zeppelin推出的ERC721的全面實施 ,我想為有興趣創建自己的ERC721令牌的開發人員編寫資源。 我花了一段時間才把頭圍上,希望如果你還沒有看ERC721 EIP ,這可以幫助你更快地到達那里。 我試圖在引入ERC721的同時平衡Solidity和ERC20的元素。 注意,有一個很棒的新網站可以合并所有合并的EIP,我強烈建議您檢查一下。
對我而言,令牌標準可以通過以下方式進行總結和比較:
了解這些操作如何工作有助于全面了解標記標準的工作原理。 以下是OpenZeppelin ERC721Token.sol的全面實施,并融合了Solidity和其他EIP的一些額外知識。 作為第一次刺戳文檔,我相信這將繼續發展,并且我會盡力保持它的更新。 任何建議的贊賞。
令牌所有權
作為迄今為止最流行的令牌標準,ERC20已經成為新令牌提案的比較標準。 他們很容易理解,至少現在我回頭看看。 在所有權方面,ERC20所涉及的是將令牌的余額映射到其各自所有者的地址:
映射(地址=> uint256)余額如果您購買了ERC20令牌,則通過您購買該令牌的合同來驗證該令牌的最終所有權,因為該合同保留了每個地址( address )有多少令牌( uint256 )的記錄。 如果我們想要轉移我們的ERC20令牌,那么我們的余額將通過balances映射進行驗證,以便我們不會嘗試發送比我們自己更多的balances 。 可能會想到的一個問題是,如果我從未與特定的令牌合同進行交互,它如何知道我的余額為零? 上面的balances映射最初默認為零,因此即使您之前從未觸及過特定的令牌合約,如果您想檢查該令牌的余額,您的余額也會被適當驗證為零。
我們一遍又一遍地聽到ERC721是如何不可互換的,再次,第100次意味著相同類別或合約的代幣可以保持不同的價值。 一個ERC721 Cryptokittie的值不等于另一個ERC721 Cryptokittie的值,因為它們都是唯一的。 為了確保這一點,我們不能再簡單地將地址映射到天平。 我們必須知道我們擁有的每個獨特的標記。
出于這個原因,在ERC721標準中,所有權由映射到您的地址的一系列令牌索引或ID確定。 由于每個令牌值都是唯一的,我們不能再簡單地查看令牌的余額 - 我們必須查看合約創建的每個單獨的令牌。 主合同保存了在該合同中創建的所有 ERC721令牌的一個運行列表,因此每個令牌在其所有ERC721令牌的上下文中都有相應的索引,該令牌可以通過allTokens數組從該特定合約中allTokens 。
uint256[] internal allTokens但是,我們也需要知道我們擁有哪些代幣,而不僅僅是合同的內容。 因此,除了整個合同中的標記索引數組之外,每個單獨的地址都有一組標記索引或標識,作為所有權映射到其地址。 我們不只是簡單地將一個地址映射到一個標記索引,因為如果一個人擁有多個標記呢? 如果我們只映射單個索引,比如說我們擁有5號令牌,并且映射到我們的地址。 然而,明天,我們購買令牌6,那么如果我們只映射了單個值,那么編號5將被我們的映射中的編號6覆蓋,并且我們將不再擁有我們擁有令牌5的記錄 - 因此需要陣列。
映射(地址=> uint256 [])內部擁有的Token這個簡單的區別刺激了ERC721令牌的許多附加要求。 使用ERC20令牌,我們正在檢查余額,但現在,我們需要根據令牌的特定索引檢查所有權。 當我們傳輸令牌時,重新排列這個數組需要進一步的需求。
那么當我們每次想驗證某個標記索引的所有權時,我們是否遍歷我們的標記數組呢? 不,有一個更簡單和更安全的方法。 相反, 除了我們擁有的我們的標記索引數組之外 ,我們還將每個標記索引或標識映射到所有者。 這樣,每次我們想知道誰擁有某個標記索引時,我們只需要提供標記索引來檢查它映射到的地址。 (這個變量包含在ERC721BasicToken.sol中 ,繼承到ERC721Token.sol。)
映射(uint256 =>地址)內部tokenOwner為什么我們除了數組之外還要這樣做? 難道我們不能只遍歷我們的令牌數組來確保我們擁有特定的令牌嗎? 我們先來問一下這個問題:如果我們傳遞標記,我們不能只添加或刪除標記索引到我們的數組嗎? 很不幸的是,不行。 回想一下,在Solidity中,我們是否應該刪除一個數組中的元素,該元素實際上并沒有被完全刪除,而是被替換為零。 例如,假設我們有一個數組myarray = [2 5 47] ,它的長度為3.然而,我們調用一個函數說明delete myarray[myarray.length.sub(1)] 。 雖然我們可能期望myarray = [2 5] ,但我們實際上有以下數組myarray = [2 5 0] ,它仍然是長度為3.我們不奇跡般地擁有id 0的標記,所以這呈現出問題。 回想一下, delete并不實際“刪除”以太坊中的值,而是將它們重置為零。 當然,在某些情況下,我們希望從地址所有權中刪除或刪除令牌。 我們寧愿重新排列我們的陣列,而不是簡單地從陣列中刪除令牌。 稍后我們會看到轉移(取消所有權)和刻錄令牌如何發揮這些信息的作用。 出于這個原因,我們也跟蹤下面的內容。 ownedTokensIndex將每個令牌id映射到其所有者數組中的相應索引。 如下所述,我們還將token標識映射到allTokens數組中的索引。
//從令牌ID映射到所有者令牌列表映射的索引(uint256 => uint256)internal ownedTokensIndex; //從令牌id映射到allTokens數組映射中的位置(uint256 => uint256)internal allTokensIndex;我們可能遇到的另一個問題是如果我們想檢查我們實際擁有多少個 ERC721令牌。 此時,我們再引入一個變量來跟蹤所有權。 (同樣,這個變量在ERC721BasicToken.sol中,并繼承到ERC721Token.sol。)
映射(地址=> uint256)內部ownedTokensCount現在,我們映射一個數字來跟蹤我們擁有的地址有多少個令牌。 當我們購買,轉讓或潛在地刻錄令牌時,此ownedTokensCount會更新。 為什么我們需要跟蹤我們擁有多少ERC721令牌? 驗證。 假設我們想將所有的ERC721令牌轉移到新的地址? 或者只是檢查我們擁有一定的金額?
在這一點上,我們可以看到如何引入唯一令牌的所有權為令牌的所有權增加了新的復雜性。 但是如何創建這些ERC721令牌?
令牌創建
回想一下,在ERC20令牌的情況下,我們映射的是令牌的平衡。 因此,為了創建ERC20令牌,我們只需要設置或增加可用的總令牌。 在ERC20設計中,我們的價值保持了我們的總可用令牌供應,總供應totalSupply_在下面。 在某些情況下,您可能已經看到ERC20令牌合約通過在構造函數中初始化的值來設置總供應量。 回想一下構造函數運行一次以初始化合約(但不是必需的)。 構造函數必須使用與合同完全相同的名稱 - 如果它不具有與合同相同的名稱,則EVM將把您期望的構造函數注冊為正常函數,這意味著任何人都可以在合同創建后調用它,很多安全漏洞取決于你在做什么。 構造函數代碼是創建合同的事務的一部分,但它不是部署位置的合同的一部分。 構造函數可用于設置初始值,所有權等。在下面, MyToken用于設置令牌的總totalSupply_量的值。 隨著需求增加以允許合同內ERC20令牌的數量變化,ERC20標準擴展到還包括一個mint函數,其中期望數量的令牌被添加到總totalSupply_量中,并且余額被相應地映射。 請注意,在下面的Transfer是一個事件 ,而不是一個函數 - 我是唯一一個花時間去尋找一個函數,而這個函數在閱讀Solidity的過程中變成一個事件嗎? 無論如何,您可以從mint功能中看到我們的余額已更新。
uint256 totalSupply_ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - //通過構造函數設置令牌供應的示例 合同MyToken { 函數MyToken(uint _setSupply) {totalSupply_ = _setSupply_} ..... //通過minting維護可變令牌供應的示例 函數mint(地址_to,uint256 _amount) onlyOwner canMint 上市 返回(布爾) {totalSupply_ = totalSupply_.add(_amount); 余額[_to] =余額[_to] .add(_amount); 薄荷(_to,_amount); 轉移(地址(0),_to,_amount); 返回true; }至于ERC721,我們了解到,由于每個單獨的令牌都是唯一的,我們必須創建每個令牌。 使用ERC20,我們可以通過添加totalSupply_來輕松創建100個批次。 但是,由于我們在ERC721標準中維護了一系列令牌,我們需要將每個令牌分別添加到該陣列中。
這里我們看到兩個函數,它們查看ERC721合同的總供應addTokenTo()和_mint() 。 我們先來addTokenTo() 。
在這里,我們從完整的實現契約中調用addTokenTo() ,然后, super.addTokenTo()允許我們首先在基本的ERC721契約中調用addTokenTo()函數。 基本上,在這兩個函數的過程中,我們更新所有全局所有權變量。 這些函數采用兩個參數_to或令牌將被擁有的地址和_tokenId或令牌的唯一_tokenId由允許調用此函數的人選擇,您可能會將此調用限制為合同所有者。 在這種情況下,用戶可以選擇任何唯一的號碼ID。 首先,在ERC721BasicToken合同中,我們檢查令牌ID是否已經擁有。 然后,我們設置所需令牌標識的令牌所有者,并為該個人帳戶的擁有令牌數加1。 回到完整的實現合約,我們還通過將這個新的標記添加到他們的ownedTokens數組的末尾并保存新標記的索引來更新新所有者( _to )標記的數組。
從上面,我們可以看到addTokenTo()更新地址給個人。 但是, allTokens數組呢? 這是_mint填補空白的地方。 在這里我們看到,當我們從完全實現的ERC721協議中調用_mint() ,我們又跳到了基本實現,這確保我們不會調用零地址并調用addTokenTo() ,盡管它很混亂,將實際回撥到我們的完整實施合同,以啟動addTokenTo()調用。 (同樣, Transfer()是一個事件,而不是一個函數。)在基本合約中的_mint()函數完成之后,回到我們的完整實現中,我們將_tokenId添加到我們的allTokensIndex的映射以及我們的allTokens數組。
從上面可以看出,盡管你可以自己調用 addTokenTo() ,但是你需要做什么才能保證全部實現ERC721合同中的所有信息是使用 _mint() 來創建新的令牌。
但是ERC721可以保存的元數據呢? 我們已經創建了令牌和令牌ID,但是他們還沒有保存任何數據。 打開Zeppelin給了我們一個例子,它是如何將映射令牌id映射到URI數據的字符串。
//可選映射令牌URI 映射(uint256 => string)內部tokenURIs;為了設置令牌的URI數據,還包括以下_setTokenURI()函數。 在這里使用您通過_mint()創建的令牌Id和所需的URI信息,可以設置映射到tokenURIs令牌ID的tokenURIs 。 注意這個函數中的要求,我們在分配數據之前確定一個令牌Id存在(意味著某人擁有它)。
盡管更復雜和更耗費精力,但我發現使用結構來存儲數據的能力,而不是映射到更有趣的索引 - 至少,創建一個帶有少量變量的不可互換的標記仍然比相反,每個“資產”創建一個智能合約。 無論如何,如果你想知道如何包含不同的數據,這些元素就是你想要改變的。
轉移和津貼
和以前一樣,我們首先回顧一下ERC20標準中的轉移和補貼是如何發生的。 我們可以使用transfer()函數直接傳輸ERC20令牌,在該函數中我們指定了要發送到的地址以及多少,該數據會根據我們的余額進行檢查,然后在主ERC20合同中進行更新。
函數傳遞(地址_to,uint256 _value)public returns(bool){require(_to!= address(0)); require(_value <= balances [msg.sender]); 余額[msg.sender] =余額[msg.sender] .sub(_value); 余額[_to] =余額[_to] .add(_value); 轉移(msg.sender,_to,_value); 返回true; }但是,我們的津貼是什么意思? 當我們希望另一份合同或地址能夠轉移我們的ERC20令牌時,我們需要允許使用ERC20合同地址為我們做到這一點 - 在分布式應用程序中的許多情況下都會出現這種需求 - 托管,游戲,拍賣等。因此,我們需要一種方法來批準其他地址來使用我們的令牌。 然后,另一個傳遞函數要求合同檢查允許誰允許花費他們的津貼。 我將從如何設置津貼開始,然后展示如何發揮轉移。
在ERC20標準中,我們有一個全局變量, allowed所有者地址映射到已批準的支出地址,然后映射一定數量的標記。 為了設置這個變量,有一個approve()函數,其中一個人能夠將批準映射到他們想要的_spender和_value 。 請注意,在這里,我們沒有檢查發件人擁有的實際數量的令牌 - 這些數據稍后會在傳輸過程中進行。 再一次, Approval是一個不是功能的事件。
//全局變量 映射(地址=>映射(地址=> uint256))內部允許 //允許另一個地址花費你的代幣 功能批準(地址_spender,uint256 _value) 上市 返回(布爾) {允許[msg.sender] [_ spender] = _value; 審批(msg.sender,_spender,_value); 返回true; }現在,一旦我們批準另一個地址來轉移我們的令牌,我們的令牌如何實際轉移? 我們批準的transferFrom()將使用下面的transferFrom()函數,在這個函數中他們將指定_from或原始擁有者的地址,接收者的地址_to和金額_value 。 在這里,我們檢查原始擁有者實際上是否擁有期望轉移的金額require(_value ≤ balances[_from]) ,然后我們檢查是否允許msg.sender通過allowed變量轉移余額,最終我們更新所有我們的映射balances以及我們allowed金額。 Tranfer再次是一個事件。 請注意,還有兩個附加功能可以允許增加( increaseApproval()批準increaseApproval() )和減少( decreaseApproval()批準decreaseApproval() )批準的分攤者津貼。
因此,我們需要再次認為,在ERC721的情況下,我們需要批準和轉讓令牌ID,而不是批準和轉移余額。 ERC721標準提供機會批準通過id傳遞令牌的地址,或者我們可以批準地址來傳輸所有的令牌。 要批準通過ID傳輸,我們使用approve()函數如下。 在這里,全局變量tokenApprovals將令牌索引或標識映射到已批準傳輸的地址。 在approve()函數中,我們首先檢查所有權或msg.sender isApprovedForAll() 。 在下文中,您可以看到,您可以使用setApprovalForAll()函數來批準一個地址來傳輸和處理由特定地址擁有的所有令牌,因為我們有一個全局變量operatorApprovals ,其中所有者的地址映射到批準的支票地址,然后映射到布爾。 默認設置為0或false,但通過使用setApprovalForAll()我們可以將此映射設置為true,并允許地址處理所有ERC721的擁有。 請注意,如果一個分配器被批準用于所有的令牌,那么他們也可以分配額外的地址支出能力。 接下來,我們使用getApproved()來檢查我們沒有設置address(0)許可。 最后,我們的tokenApprovals映射完成到所需的地址。 和ERC20一樣, Approval是事件。
現在,我們來到我們如何實際轉移ERC721的。 全面實施實際上提供了兩種轉移方式。 第一種方法是不鼓勵的,但讓我們回過頭來理解。 在transferFrom() ,發送者和接收者地址與_tokenId一起指定傳輸,我們使用修飾符canTransfer()來確保msg.sender被批準傳輸令牌或擁有它。 在檢查發件人和收件人地址有效后, clearApproval()函數用于從原始令牌的所有者中移除批準轉讓,以便以前批準的支票人可能不會繼續轉移令牌。 接下來,在ERC721完整實現合約中調用removeTokenFrom() ,類似于使用super的removeTokenFrom()函數在ERC721基本實現中調用removeTokenFrom()函數。 您可以看到從擁有的ownedTokensCount映射( tokenOwner映射)中移除了該令牌,還有一個tokenOwner是我們將擁有者ownedTokens數組中的最后一個令牌移動到正在傳輸的令牌的索引,并將數組縮短一個見22-30行)。 最后,我們使用addTokenTo()函數將此令牌索引添加到其新所有者。 Transfer是一個事件。
現在,有一個問題需要問,我們如何確保我們將ERC721發送給可處理額外轉帳的合同? 我們知道如果需要,外部擁有賬戶(EOA)可以使用我們的ERC721完整實施合同交易代幣; 然而,如果我們將令牌發送給一個沒有相應功能的合同,通過我們原始的ERC721合同進行交易和轉讓令牌,那么由于無法將令牌拿出來,導致令牌有效丟失。 這種情緒反映了通過ERC223提案所揭示的許多關注, ERC223提案是對ERC20提出的修改建議,以防止這些錯誤轉讓。
為了避免問題和標準化,ERC721完整實施標準引入了safeTransferFrom()函數。 在深入研究這些工作之前,讓我們看看一些額外的需求,在這些需求中我們有一個實現ERC721Receiver.sol接口的ERC721Holder.sol合約。 ERC721Holder.sol合同將成為您希望持有ERC721令牌的錢包,拍賣或經紀合同的一部分。 這個標準的原因可以追溯到EIP165 ,其目標是創建“一種標準的方法來發布和檢測智能合約實現的接口。”我們如何檢測接口? 下面我們將看到一個“魔術值” ERC721_RECEIVED ,它是onERC721Received()函數的函數簽名。 函數簽名是規范簽名字符串散列的前四個字節。 在這種情況下,它按bytes4(keccak256(“onERC721Received(address, uint256, bytes)”)) ,如下所述。 什么是函數簽名用于? 在字節碼中找到包含被調用函數調用代碼的位置。 合同中的每個功能都會擁有自己的簽名,當您打電話給您的合同時,EVM會使用一系列切換案例來查找與您的呼叫相匹配的功能簽名并相應地執行您的代碼。 因此,在我們的ERCHolder合同中,我們看到onERCReceived()函數簽名將匹配ERC721Receiver接口中的ERC721_RECEIVED變量。
您的ERC721Holder合同不是處理ERC721令牌的完整合同。 該模板旨在為您提供標準化接口,以驗證是否使用了ERC721Receiver標準接口。 您需要擴展或繼承ERC721Holder合同,在您的錢包或拍賣合同中包含處理ERC721的功能。 即使托管代幣,您也需要添加功能,以便持有人合同可以根據需要撥打電話將合約轉出合同。
現在,回到我們原來的ERC721合同, safeTransferFrom()工作方式如下 - 您可以使用選項1進行傳輸,其中safeTransferFrom()函數不包含附加數據,或者您可以使用選項2將數據包含在bytes _data的形式。 與之前一樣, transferFrom()函數用于從_from地址中刪除標記所有權并將標記所有權添加到_to地址。 但是,我們有一個額外的要求,即運行checkAndCallSafeTransfer()函數。 首先,我們通過使用AddressUtils.sol庫檢查_to地址是否是一個實際的合同 - 我在下面包含了函數isContract() ,以便您快速了解它正在做什么。 如前所述,目前研究和開發允許以太坊的外部擁有賬戶(EOAs)也維護他們自己的代碼,所以無論何時何時出現,都需要注意這樣的支票。 在驗證_to是合同地址后,我們檢查調用onERC721Received()函數是否會返回我們期望從標準接口獲得的相同函數簽名。 如果沒有返回正確的值,那么transferFrom()函數會回滾,因為我們已經確定_to沒有實現預期的接口。
噢,我們有它。 傳輸ERC721令牌。 現在,刻錄令牌應該看起來很容易。
燃燒
至于ERC20,由于我們只操縱單一映射余額,因此我們只需要燒毀或銷毀特定地址的令牌,這可以是用戶或合約。 在下面的burn() ,我們指定了我們想通過_value變量刻錄的令牌的數量。 要燒的地址是msg.sender ,所以我們更新它們各自的余額,然后我們也減少令牌的總totalSupply_量。 這里的Burn和Transfer是事件。
對于ERC721令牌,我們需要確保特定令牌ID或索引已被刪除。 與addTokenTo()和_mint()函數非常相似,我們的_burn()函數使用super在我們的基本ERC721實現中調用函數。 首先,我們clearApproval() ,然后通過removeTokenFrom()從所有權中刪除令牌,并使用Transfer事件在前端警告此更改。 接下來,我們通過刪除映射到特定標記索引的內容來消除與該標記關聯的元數據。 最后,就像從所有權中刪除令牌一樣,我們重新排列我們的allTokens數組,以便用數組中的最后一個令牌替換_tokenId索引。
如果你完成了,感謝閱讀! 我想最大的挑戰將是如何適應這些標準,如何將ERC721與所需的元數據一起鑄造,以及如何確保基于獨特價值交換的轉移。 目前已經有很多例子 - 當然,著名的Cryptokitties , Cryptogs (來自我的EthDenver團隊), Cryptocelebrities , Decentraland ,如果您訪問OpenSea,您可以找到大量數字資產和收藏品。 我可以想象這個標準有更多的用例 - 希望這篇文章有助于讓你了解......!
https://medium.com/blockchannel/walking-through-the-erc721-full-implementation-72ad72735f3c
總結
以上是生活随笔為你收集整理的全面理解ERC721的实现机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: UTXO与账户/余额模型
- 下一篇: 在Google的GPU上永远免费训练您的