全面理解ERC721的實現機制
TL; DR
基本上,由於ERC721的所有權基於唯一索引或ID的所有權,因此需要將令牌建立和傳輸的基本原理外推以適應這種情況。 此外,最新的完整實現還包括一個safeTransferFrom()
函式,用於在傳輸令牌之前檢查標準介面的實現。
ERC721令牌
圍繞對ERC721的興趣,我已經看到了許多關於可以儲存元資料的非可替換標記的材料,但是我發現的材料深度讓我尋找更多細節。 我對ERC721的興趣始於2月份的EthDenver--您可以在這裡閱讀關於我們使用ERC721的專案。 此後,我對ERC721標準的實施進行了更新,因為我看到由此設計帶來的更多價值。 作為本週從Open Zeppelin推出的ERC721
對我而言,令牌標準可以通過以下方式進行總結和比較:
- 所有權 - 如何處理令牌所有權?
- 創造 - 如何建立令牌?
- 轉讓和津貼 - 轉讓令牌的方式,以及我們如何允許其他地址(合同或外部擁有賬戶)的轉讓能力?
- 燒傷 - 我們如何燃燒或毀滅令牌?
瞭解這些操作如何工作有助於全面瞭解標記標準的工作原理。
令牌所有權
作為迄今為止最流行的令牌標準,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令牌,我們正在檢查餘額,但現在,我們需要根據令牌的特定索引檢查所有權。 當我們傳輸令牌時,重新排列這個陣列需要進一步的需求。
那麼當我們每次想驗證某個標記索引的所有權時,我們是否遍歷我們的標記陣列呢? 不,有一個更簡單和更安全的方法。 相反, 除了我們擁有的我們的標記索引陣列之外 ,我們還將每個標記索引或標識對映到所有者。 這樣,每次我們想知道誰擁有某個標記索引時,我們只需要提供標記索引來檢查它對映到的地址。
對映(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()
函式。 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與所需的元資料一起鑄造,以及如何確保基於獨特價值交換的轉移。 我可以想象這個標準有更多的用例 - 希望這篇文章有助於讓你瞭解......!
https://medium.com/blockchannel/walking-through-the-erc721-full-implementation-72ad72735f3c