層級化NFT標准誕生:EIP-6150
前言
因為一些機緣,我最近的幾個同行朋友一起提交了一個新的 EIP 協議標准,EIP-6150,這是一個支持層級結構的 NFT 協議標准,撰寫此文時處在 Review 狀態,改為 Last Call 狀態的 PR 還在等待通過。
該協議標准有 4 位作者:Keegan Lee、msfew、Kartin 和 qizhou。Keegan Lee 就是我,主要負責了接口的定義和實現代碼的編寫。Kartin 是這個 EIP 的發起人,也是 Hyper Oracle 的創始人。msfew 則是 Hyper Oracle 的研究員,主要幫忙做一些輔助性的工作,包括完善文檔、提交 PR、跟進討論區的 QA 等。qizhou 是 EthStorage 的創始人,之前就提交過其他 EIP,熟悉申請 EIP 的流程,也對以太坊基金會的人比較熟悉,為這個協議提供了很多指導。以下是該 EIP-6150 的 github 地址:
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-6150.md
Hierarchical NFTs
目前的 NFT 幾乎都是扁平化的,不同 NFT 之間不存在關聯關系。而層級化的 NFT,則可以將所有 NFT 串聯起來組成樹狀結構,就像文件系統一樣。
可以想象成每個文件夾都是一個單獨的 NFT,不同文件夾之間的層級關系也就是 NFT 之間的層級關系。
層級化的 NFT 可用於多種不同的應用場景,比如:
組織架構
社交關系圖譜
電商商品類目結構
層級評論系統
可以說,任何具有層級結構的場景都可以適用這個 EIP-6150 協議標准。層級結構的 NFT 在去中心化社交、去中心化電商等領域都將可能產生廣泛應用。
接口定義
EIP-6150 總共定義了 5 個接口文件:
IERC6150
IERC6150Enumerable
IERC6150Burnable
IERC6150ParentTransferable
IERC6150AccessControl
IERC6150
IERC6150 是規定必須實現的接口,最小化定義了一個事件和四個函數,且要求繼承 IERC165 和 IERC721 接口,接口定義如下:
/** * @title ERC-6150 Hierarchical NFTs Token Standard * @dev See https://eips.ethereum.org/EIPS/eip-6150 * Note: the ERC-165 identifier for this interface is 0x897e2c73. */interface IERC6150 is IERC721, IERC165 { /** * @notice Emitted when `tokenId` token under `parentId` is minted. * @param minter The address of minter * @param to The address received token * @param parentId The id of parent token, if it's zero, it means minted `tokenId` is a root token. * @param tokenId The id of minted token, required to be greater than zero */ event Minted( address indexed minter, address indexed to, uint256 parentId, uint256 tokenId ); /** * @notice Get the parent token of `tokenId` token. * @param tokenId The child token * @return parentId The Parent token found */ function parentOf(uint256 tokenId) external view returns (uint256 parentId);
/** * @notice Get the children tokens of `tokenId` token. * @param tokenId The parent token * @return childrenIds The array of children tokens */ function childrenOf( uint256 tokenId ) external view returns (uint256[] memory childrenIds);
/** * @notice Check the `tokenId` token if it is a root token. * @param tokenId The token want to be checked * @return Return `true` if it is a root token; if not, return `false` */ function isRoot(uint256 tokenId) external view returns (bool);
/** * @notice Check the `tokenId` token if it is a leaf token. * @param tokenId The token want to be checked * @return Return `true` if it is a leaf token; if not, return `false` */ function isLeaf(uint256 tokenId) external view returns (bool); }
Minted 事件需在鑄造一個新的 NFT 時發出,記錄了新 NFT 的鑄造者(minter)、接收者(to)、父節點 NFT 的 ID(parentId)、新 NFT ID(tokenId)。當鑄造一個根節點 NFT 時,那 parentId 則記為 0,即 0 表示一個無效的空節點,因此,有效的節點 NFT 的 tokenId 就不可以為 0。
parentOf 函數用於查詢指定 tokenId 的 NFT 的父節點 NFT。
childrenOf 函數則查詢出指定 tokenId 的 NFT 的所有子節點 NFTs。
isRoot 和 isLeaf 函數則分別可查詢指定 tokenId 在整個 NFT 層級樹中是不是根節點或葉子節點。
IERC6150Enumerable
IERC6150Enumerable 是可選的擴展接口,主要補充了幾個跟層級相關的 Enumerable 的查詢接口,接口定義如下:
/** * @title ERC-6150 Hierarchical NFTs Token Standard, optional extension for enumerable * @dev See https://eips.ethereum.org/EIPS/eip-6150 * Note: the ERC-165 identifier for this interface is 0xba541a2e. */interface IERC6150Enumerable is IERC6150, IERC721Enumerable { /** * @notice Get total amount of children tokens under `parentId` token. * @dev If `parentId` is zero, it means get total amount of root tokens. * @return The total amount of children tokens under `parentId` token. */ function childrenCountOf(uint256 parentId) external view returns (uint256);
/** * @notice Get the token at the specified index of all children tokens under `parentId` token. * @dev If `parentId` is zero, it means get root token. * @return The token ID at `index` of all chlidren tokens under `parentId` token. */ function childOfParentByIndex( uint256 parentId, uint256 index ) external view returns (uint256);
/** * @notice Get the index position of specified token in the children enumeration under specified parent token. * @dev Throws if the `tokenId` is not found in the children enumeration. * If `parentId` is zero, means get root token index. * @param parentId The parent token * @param tokenId The specified token to be found * @return The index position of `tokenId` found in the children enumeration */ function indexInChildrenEnumeration( uint256 parentId, uint256 tokenId ) external view returns (uint256); }
繼承 IERC721Enumerable 也是可選的,但為了更好地兼容 ERC721,最好可以繼承。childrenCountOf 函數用於查詢指定節點下有多少個子節點,如果參數 parentId 為 0,則表示查詢根節點的數量。childOfParentByIndex 函數則是從指定的父節點下的所有子節點數組中找出指定索引位置的子節點的 tokenId,比如指定父節點 parentId = 110,其下有 10 個子節點,找出索引位置為 5 的子節點 tokenId = 1105,則查詢結果返回 1105。indexInChildrenEnumeration 函數則是查詢指定的 tokenId 在指定父節點下的子節點數組中所在的索引位置,比如指定 tokenId = 1105,父節點 parentId = 110,1105 在子節點數組中的索引位置為 5,則查詢結果返回 5。如果指定 tokenId 並不在指定父節點 parentId 下面,則需要拋出錯誤。
IERC6150Burnable
IERC6150Burnable 也是可選的擴展接口,定義了銷毀節點的操作,接口定義如下:
/** * @title ERC-6150 Hierarchical NFTs Token Standard, optional extension for burnable * @dev See https://eips.ethereum.org/EIPS/eip-6150 * Note: the ERC-165 identifier for this interface is 0x4ac0aa46. */interface IERC6150Burnable is IERC6150 { /** * @notice Burn the `tokenId` token. * @dev Throws if `tokenId` is not a leaf token. * Throws if `tokenId` is not a valid NFT. * Throws if `owner` is not the owner of `tokenId` token. * Throws unless `msg.sender` is the current owner, an authorized operator, or the approved address for this token. * @param tokenId The token to be burnt */ function safeBurn(uint256 tokenId) external;
/** * @notice Batch burn tokens. * @dev Throws if one of `tokenIds` is not a leaf token. * Throws if one of `tokenIds` is not a valid NFT. * Throws if `owner` is not the owner of all `tokenIds` tokens. * Throws unless `msg.sender` is the current owner, an authorized operator, or the approved address for all `tokenIds`. * @param tokenIds The tokens to be burnt */ function safeBatchBurn(uint256[] memory tokenIds) external; }
只定義了兩個函數,safeBurn 用於安全銷毀單個節點,但要求只有指定節點為葉子節點時才允許銷毀。就和 Linux 的文件系統一樣,如果某目錄下存在其他文件或文件夾,是不允許直接刪除的。若強制刪除,則目錄下的所有文件和文件夾都會被級聯式全部刪除。當前協議沒有定義級聯式刪除的函數,若有這個需求,可以自己再額外去添加函數實現。safeBatchBrun 函數則是用於批量銷毀多個葉子節點。
IERC6150ParentTransferable
IERC6150ParentTransferable 也是一個可選的擴展接口,支持層級關系的轉移,就和文件夾可以從一個目錄移動到另一個目錄一樣,接口定義如下:
/** * @title ERC-6150 Hierarchical NFTs Token Standard, optional extension for parent transferable * @dev See https://eips.ethereum.org/EIPS/eip-6150 * Note: the ERC-165 identifier for this interface is 0xfa574808. */interface IERC6150ParentTransferable is IERC6150 { /** * @notice Emitted when the parent of `tokenId` token changed. * @param tokenId The token changed * @param oldParentId Previous parent token * @param newParentId New parent token */ event ParentTransferred( uint256 tokenId, uint256 oldParentId, uint256 newParentId ); /** * @notice Transfer parentship of `tokenId` token to a new parent token * @param newParentId New parent token id * @param tokenId The token to be changed */ function transferParent(uint256 newParentId, uint256 tokenId) external;
/** * @notice Batch transfer parentship of `tokenIds` to a new parent token * @param newParentId New parent token id * @param tokenIds Array of token ids to be changed */ function batchTransferParent( uint256 newParentId, uint256[] memory tokenIds ) external; }
接口定義了兩個函數和一個事件,支持單節點的轉移,也支持多節點的批量轉移。每個節點發生層級關系轉移時,需要拋出 ParentTransferred 事件,記錄下所轉移的 tokenId、舊的父節點 ID 和新的父節點 ID。transferParent 將指定的 tokenId 轉移到指定的父節點下,若指定的父節點為 0,則表示指定節點改為了根節點。batchTransferParent 則可以指定多個 tokenId,批量將這些節點都轉移到指定的父節點下。
IERC6150AccessControl
最後一個接口 IERC6150AccessControl 也是可選的,提供了幾個權限控制的函數,接口定義如下:
/** * @title ERC-6150 Hierarchical NFTs Token Standard, optional extension for access control * @dev See https://eips.ethereum.org/EIPS/eip-6150 * Note: the ERC-165 identifier for this interface is 0x1d04f0b3. */interface IERC6150AccessControl is IERC6150 { /** * @notice Check the account whether a admin of `tokenId` token. * @dev Each token can be set more than one admin. Admin have permission to do something to the token, like mint child token, * or burn token, or transfer parentship. * @param tokenId The specified token * @param account The account to be checked * @return If the account has admin permission, return true; otherwise, return false. */ function isAdminOf( uint256 tokenId, address account ) external view returns (bool);
/** * @notice Check whether the specified parent token and account can mint children tokens * @dev If the `parentId` is zero, check whether account can mint root nodes * @param parentId The specified parent token to be checked * @param account The specified account to be checked * @return If the token and account has mint permission, return true; otherwise, return false. */ function canMintChildren( uint256 parentId, address account ) external view returns (bool);
/** * @notice Check whether the specified token can be burnt by specified account * @param tokenId The specified token to be checked * @param account The specified account to be checked * @return If the tokenId can be burnt by account, return true; otherwise, return false. */ function canBurnTokenByAccount( uint256 tokenId, address account ) external view returns (bool); }
總共定義了三個函數,isAdminOf 用於查詢指定的 account 對指定的 tokenId 是否有管理員權限。在 ERC721 中,每個 NFT 都只有唯一的 owner 並擁有管理權限。但在層級式的結構中,一個 NFT 是可以有多個管理員的,就和文件系統中可以有多個管理員一樣。這個擴展接口就提供了支持多管理員的模式,但對於怎么設置多個管理員,則難以定義通用函數,所以就沒做標准化的定義。不過,需要保證,NFT 的 owner 同時也是管理員。
canMintChildren 則用來判定某個 account 對指定的 parentId 是否具有鑄造子節點的權限。
canBurnTokenByAccount 則用來檢查某個 account 對指定的 tokenId 是否具有銷毀的權限。
參考實現
EIP 的 github 上,我對每個接口都提供了對應的參考實現代碼,代碼地址如下:
https://github.com/ethereum/EIPs/tree/master/assets/eip-6150/contracts
但這裏我不打算對每個實現代碼都一一講解,我只講最核心的 ERC6150.sol 的實現。因為代碼相對有點長,就不貼代碼出來了,大家可以點擊鏈接進去看代碼。我主要講講實現的一些邏輯和思路。
存儲上,用了三個 mapping:_parentOf、_childrenOf、_indexInChildrenArray,分別用來存儲指定節點的:父節點、子節點數組、所在子節點數組裏的索引位置。有了這三個 mapping 之後,幾個查詢函數的實現就非常簡單了,我就不細說了。核心是 _safeMintWithParent 和 _safeBurn 函數,分別是鑄造 NFT 和銷毀 NFT 的內部函數。
鑄造函數的代碼如下:
function _safeMintWithParent( address to, uint256 parentId, uint256 tokenId, bytes memory data ) internal virtual { require(tokenId > 0, "ERC6150: tokenId is zero"); if (parentId != 0) require(_exists(parentId), "ERC6150: parentId doesn't exist");
_beforeMintWithParent(to, parentId, tokenId);
_parentOf[tokenId] = parentId; _indexInChildrenArray[tokenId] = _childrenOf.length; _childrenOf[parentId].push(tokenId);
_safeMint(to, tokenId, data); emit Minted(msg.sender, to, parentId, tokenId);
_afterMintWithParent(to, parentId, tokenId); }
實現邏輯其實也很簡單,有兩個校驗需要注意下,一是要鑄造的新 NFT 的 tokenId 需要大於 0,正如前面所說的,0 為無效節點;二是當 parentId 不為 0 時,需保證 parentId 是存在的,當 parentId 為 0 時,則表示鑄造的是根節點 NFT。 _beforeMintWithParent 和 _afterMintWithParent 是為了增加擴展性而增加的,可由繼承此合約的上層合約根據需求再去實現。中間代碼就是對幾個 mapping 進行賦值了,然後調用了 ERC721 的 _safeMint 函數實現底層的鑄造,接着就發送 Minted 事件了。
這個鑄造函數是 internal virtual 的,上層合約可以重載該函數,且上層的實現合約需要再根據具體需求自己添加开放的鑄造函數。
接着看看銷毀函數,代碼如下:
function _safeBurn(uint256 tokenId) internal virtual { require(_exists(tokenId), "ERC6150: tokenId doesn't exist"); require(isLeaf(tokenId), "ERC6150: tokenId is not a leaf");
uint256 parent = _parentOf[tokenId]; uint256 lastTokenIndex = _childrenOf[parent].length - 1; uint256 targetTokenIndex = _indexInChildrenArray[tokenId]; uint256 lastIndexToken = _childrenOf[parent][lastTokenIndex]; if (lastTokenIndex > targetTokenIndex) { _childrenOf[parent][targetTokenIndex] = lastIndexToken; _indexInChildrenArray[lastIndexToken] = targetTokenIndex; } delete _childrenOf[parent][lastIndexToken]; delete _indexInChildrenArray[tokenId]; delete _parentOf[tokenId];
_burn(tokenId); }
銷毀時,要求 tokenId 是存在的且需是葉子節點才允許銷毀。另外,銷毀時,需要從子節點數組中移除,而為了節省 gas,同時把子節點數組中的最後一個元素移到了銷毀的索引位置。
另外,實現代碼中,也封裝了批量鑄造的內部函數,方便擴展支持批量鑄造多個子節點的需求。
其實,整個協議並不復雜,但已經足以覆蓋到很多應用場景,後續我會結合一些具體的應用場景,再增加示例代碼作為案例,以促進該協議的落地應用。
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。
XRP 漲至 7.5 美元?分析師告訴 XRP 大軍為純粹的煙火做好准備!
加密貨幣分析師 EGRAG 表示,XRP 即將迎來關鍵時刻,價格可能大幅上漲,這取決於能否突破關鍵...
今晚ETH迎來暴漲時代 op、arb、metis等以太坊二層項目能否跑出百倍幣?
北京時間7月23日晚上美股开盤後 ETH 的ETF开始交易。ETH的裏程碑啊,新的時代开啓。突破前...
Mt Gox 轉移 28 億美元比特幣 加密貨幣下跌 ETH ETF 提前發行
2014 年倒閉的臭名昭著的比特幣交易所 Mt Gox 已向債權人轉移了大量比特幣 (BTC),作...