Skip to content

SIP-721 標準

簡要說明

非同質化代幣(Non-Fungible Token,以下簡稱 NFT 或 NFTs)的標準介面。

摘要

提案描述了在智能合約中實現 NFT 的標準API。該標準提供了追踪和轉移 NFT 的基本功能。

提案考慮了個人擁有和交易 NFT 的場景,以及委託給第三方(如:代理人/錢包/拍賣人,稱為 "操作者")的情況。

NFT 可以代表對數字或實物資產的所有權。我們考慮了資產的多樣性,並且我們知道您會想要代表更多:

  • 實體財產 - 如:房屋,獨特的藝術品
  • 虛擬收藏品 - 如:小貓,收藏卡的獨特圖片
  • "負" 資產 - 如:貸款等

總的來說,所有房屋都是不同的,沒有兩只小貓是一樣的。NFT 是可區分的,必須分別追踪每個所有者。

動機

標準介面可以方便錢包/代理/拍賣應用與以太坊上的任何 NFT 進行交互。提供簡單的 ERC-721 智能合約以及追踪數量眾多 NFT 的合約。其他應用將在下面討論。

該標準的靈感來自 ERC20 代幣標準,並建立自 EIP-20 創建以來的兩年經驗。ERC20 無法追踪 NFT,因為每種資產都是不同的(不可替代的),而 ERC20 每個代幣的數量都是相同的(可替代的)。

該標準與 ERC20 之間的差異在下面進行說明。

規範

**每個符合 ERC-721 的合約都必須實現 ERC721ERC165 介面(受以下「說明」約束):

solidity
pragma solidity ^0.4.20;

/// @title ERC-721非同质化代币标准
/// @dev 參考 https://learnblockchain.cn/docs/eips/eip-721.html
///  注意:ERC-165 接口id為 0x80ac58cd。
interface ERC721 /* is ERC165 */ {
    /// @dev 當任何NFT的所有權更改時(不管哪種方式),就會觸發此事件。
    ///  包括在創建時(`from` == 0)和銷毀時(`to` == 0),合約創建時除外。
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

    /// @dev 當更改或確認NFT的授權地址時觸發。
    ///  零地址表示沒有授權的地址。
    ///  發生 `Transfer` 事件時,同樣表示該NFT的授權地址(如果有)被重置為“無”(零地址)。
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    /// @dev 所有者啟用或禁用操作員時觸發。(操作員可管理所有者所持有的NFTs)
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    /// @notice 統計所持有的NFTs數量
    /// @dev NFT 不能分配給零地址,查詢零地址同樣會拋出異常
    /// @param _owner : 待查地址
    /// @return 返回數量,也許是0
    function balanceOf(address _owner) external view returns (uint256);

    /// @notice 返回所有者
    /// @dev NFT 不能分配給零地址,查詢零地址拋出異常
    /// @param _tokenId NFT 的id
    /// @return 返回所有者地址
    function ownerOf(uint256 _tokenId) external view returns (address);

    /// @notice 將NFT的所有權從一個地址轉移到另一個地址
    /// @dev 如果`msg.sender` 不是當前的所有者(或授權者)拋出異常
    /// 如果 `_from` 不是所有者、`_to` 是零地址、`_tokenId` 不是有效id 均拋出異常。
    ///  當轉移完成時,函數檢查  `_to` 是否是合約,如果是,調用 `_to`的 `onERC721Received` 並且檢查返回值是否是 `0x150b7a02`(即:`bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`)  如果不是拋出異常。
    /// @param _from :當前的所有者
    /// @param _to :新的所有者
    /// @param _tokenId :要轉移的token id.
    /// @param data 其他參數
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

    /// @notice 將NFT的所有權從一個地址轉移到另一個地址
    /// @dev 當轉移完成時,函數檢查 `_to` 是否是合約,如果是,調用 `_to`的 `onERC721Received` 並且檢查返回值是否是 `0x150b7a02`(即:`bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`)  如果不是拋出異常。
    ///  當轉移完成時,發出一個 `Transfer` 事件。
    /// @param _from :當前的所有者
    /// @param _to :新的所有者
    /// @param _tokenId :要轉移的token id.
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice 將NFT的所有權從一個地址轉移到另一個地址
    /// @dev 如果`msg.sender` 不是當前的所有者(或授權者)拋出異常
    /// 如果 `_from` 不是所有者、`_to` 是零地址、`_tokenId` 不是有效id 均拋出異常。
    /// @param _from :當前的所有者
    /// @param _to :新的所有者
    /// @param _tokenId :要轉移的token id.
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice 批准地址的NFT,這個地址可以操作所有的NFT。
    /// @dev 在`Approval`事件觸發之前,合約必須允許所有者通過`ownerOf`函數查詢NFT。
    /// @param _approved :權限被授權給的地址。
    /// @param _tokenId :NFT的id。
    function approve(address _approved, uint256 _tokenId) external payable;

    /// @notice 批准或撤銷對另一個地址的授權操作。
    /// @dev 實現合約必須要檢查 `operator` 是否能操作所有者 `_owner` 所持有的NFT。
    /// @param _operator : 操作者的地址。
    /// @param _approved : 是否批准操作者。
    function setApprovalForAll(address _operator, bool _approved) external;

    /// @notice 獲取操作者對所有者地址的授權狀態。
    /// @param _owner : 所有者的地址。
    /// @param _operator : 操作者的地址。
    /// @return 返回真,如果操作者被授權操作所有者地址的NFT。
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

interface ERC165 {
    /// @notice 判斷合約是否實現了接口
    /// @param interfaceID ERC-165 定義的接口 ID
    /// @dev 函數執行需要少於 30,000 gas。
    /// @return 如果合約實現了 interfaceID(不等於 0xffffffff),則返回 true,否則返回 false。
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

如果合約(應用)要接受NFT的安全轉賬,則必須實現以下接口

solidity
/// @dev 根據 ERC-165 標準,介面 ID 為 0x150b7a02。
interface ERC721TokenReceiver {
    /// @notice 處理接收NFT
    /// @dev 在 ERC721 智能合約的 `transfer` 函數完成後,在接收地址上調用此函數。
    /// 函數可以通過 revert 拒絕接收。返回非 `0x150b7a02` 也是拒絕接收。
    /// 注意:調用此函數的 `msg.sender` 是 ERC721 的合約地址。
    /// @param _operator:調用 `safeTransferFrom` 函數的地址。
    /// @param _from:之前的NFT擁有者。
    /// @param _tokenId:NFT token ID。
    /// @param _data:附加數據。
    /// @return 正確處理時返回 `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}

以下元信息擴展是可選的(查看後面的 "說明" 部分),但可以提供一些資產代表的信息以便查詢。

solidity
/// @title ERC-721非同質化代幣標準,可選元信息擴展
/// @dev 參考 https://learnblockchain.cn/docs/eips/eip-721.html
///  注意:根據 ERC-165 標準,介面 ID 為 0x5b5e139f。
interface ERC721Metadata /* is ERC721 */ {
    /// @notice NFTs 集合的名稱
    function name() external view returns (string _name);

    /// @notice NFTs 縮寫代號
    function symbol() external view returns (string _symbol);

    /// @notice 一個給定資產的唯一統一資源標識符(URI)
    /// @dev 如果 `_tokenId` 無效,拋出異常。URIs 在 RFC 3986 定義,
    /// URI 也許指向一個符合 "ERC721 元數據 JSON Schema" 的 JSON 文件。
    function tokenURI(uint256 _tokenId) external view returns (string);
}

以下是 "ERC721 元數據 JSON Schema" 的描述:

json
{
    "title": "Asset Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "指示NFT代表什麼"
        },
        "description": {
            "type": "string",
            "description": "描述NFT 代表的資產"
        },
        "image": {
            "type": "string",
            "description": "指向NFT表示資產的資源的URI(MIME 類型為 image/*) , 可以考慮寬度在320到1080像素之間,寬高比在1.91:1到4:5之間的圖像。"
        }
    }
}

以下枚舉擴展信息是可選的(查看後面的 "說明" 部分),但可以提供NFTs的完整列表,以便NFT可被發現。

solidity
/// @title ERC-721非同質化代幣標準枚舉擴展信息
/// @dev 參考 https://learnblockchain.cn/docs/eips/eip-721.html
///  注意:根據 ERC-165 標準,介面 ID 為 0x780e9d63。
interface ERC721Enumerable /* is ERC721 */ {
    /// @notice  NFTs 計數
    /// @return  返回合約有效跟踪(所有者不為零地址)的 NFT 數量
    function totalSupply() external view returns (uint256);

    /// @notice 枚舉索引NFT
    /// @dev 如果 `_index` >= `totalSupply()` 則拋出異常
    /// @param _index 小於 `totalSupply()` 的索引號
    /// @return 對應的 token id(標準不指定排序方式)
    function tokenByIndex(uint256 _index) external view returns (uint256);

    /// @notice 枚舉索引某個所有者的 NFTs
    /// @dev  如果 `_index` >= `balanceOf(_owner)` 或 `_owner` 是零地址,拋出異常
    /// @param _owner 查詢的所有者地址
    /// @param _index 小於 `balanceOf(_owner)` 的索引號
    /// @return 對應的 token id (標準不指定排序方式)
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}

說明

Solidity 0.4.20 介面語法不足以表達 ERC-721 標準文件,ERC-721 標準的合約還必須遵守以下規則:

  • Solidity issue #3412:上述介面中的每個函數都包含明確的可變性聲明。可變性聲明按照從弱到強的順序為:payable(可支付),隱含的不可支付,view(視圖)和 pure(純函數)。實現必須符合此介面中的可變性聲明,或者使用更強的可變性聲明。例如,介面中的 payable 函數可以在合約中實現為不可支付(即未指定)。

  • Solidity issue #3419:同時實現 ERC721Metadata 和 ERC721Enumerable 介面也需要實現 ERC721,ERC-721 要求實現 ERC-165 介面。

  • Solidity issue #2330:標記為 external 的函數也可以使用 public 可見性。

原理闡述

許多智能合約提案依賴於追蹤可區分的資產,例如 Decentraland 的 LAND、CryptoPunk 中的獨特人物,以及使用 DMarket 或 EnjinCoin 等系統的遊戲內物品。未來的應用包括追蹤現實世界的資產,例如房地產(如Ubitquity或Propy等公司所設想的)。在這些情況下,這些項目不能像數字資產一樣集中於一個賬本中,而是每個資產都必須單獨且原子地追蹤所有權。無論資產的性質如何,如果擁有一個標準化接口,允許跨行業的資產管理和交易平台,那麼生態系統將變得更加強大。

"NFT" 詞的選擇

"NFT" 幾乎是所有受調查者都滿意的詞,並且廣泛應用於可區分的數字資產領域。

考慮的替代方案:可區分的資產、所有權、代幣(通證)、資產、權益、票據

NFT 身份識別號碼

每個NFT都由ERC-721智能合約內部的唯一uint256 ID識別。該識別碼在整個協議期間不得更改。(合約地址,tokenId)對將成為以太坊區塊鏈上特定資產的全球唯一且完全合格的標識符。儘管某些ERC-721智能合約可能方便地以ID為0開始並為每個新的NFT加1,但呼叫者不得假設ID號具有任何特定的模式,並且必須將ID視為黑盒。另請注意,NFT可能會變得無效(被銷毀)。請參閱支持的列舉接口的列舉功能。

由於UUID和sha3哈希可以直接轉換為uint256,因此使用uint256可以實現更廣泛的應用。

代幣轉移(或稱轉賬)

ERC-721標準有兩個轉移函數safeTransferFrom(重載了帶data和不帶data參數的兩種形式)及transferFrom,轉移可以由以下角色發起:

  • NFT的所有者
  • NFT的被授權(approved)地址
  • NFT當前所有者授權的(authorized)操作員

此外,授權的操作員也可以為NFT設置授權(approved)地址,這可以為錢包、經紀人和拍賣應用提供一套強大的工具,方便快速使用大量的NFT。

轉移方法僅列出了特定條件下需要拋出異常的情況,我們自己的實現也可以在其他情況下拋出異常,這可以實現一些有趣效果:

  • 如果合約暫停,可以禁用轉賬,如CryptoKitties合約(611行)
  • 將接收NFT的某些地址列入黑名單,如CryptoKitties合約(565, 566行)
  • 禁用非安全的轉賬,除非_tomsg.sendercountOf(_to)為非零或之前是非零(這些情況是安全的),否則transferFrom拋出異常。
  • 向交易雙方收取費用,從零地址向非零地址授權(approve)可以要求支付費用,還原到(approve)零地址時退款;調用任何transfer時要求支付費用;要求transfer參數_to等於msg.sender;要求transfer參數_to是NFT授權(approved)的地址
  • 僅讀的NFT註冊表,使用函數unsafeTransfertransferFromapprove以及setApprovalForAll時都拋出異常。异常。