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 的合約都必須實現 ERC721
和 ERC165
介面(受以下「說明」約束):
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的安全轉賬,則必須實現以下接口。
/// @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);
}
以下元信息擴展是可選的(查看後面的 "說明" 部分),但可以提供一些資產代表的信息以便查詢。
/// @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" 的描述:
{
"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可被發現。
/// @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行)
- 禁用非安全的轉賬,除非
_to
為msg.sender
或countOf(_to)
為非零或之前是非零(這些情況是安全的),否則transferFrom
拋出異常。 - 向交易雙方收取費用,從零地址向非零地址授權(
approve
)可以要求支付費用,還原到(approve
)零地址時退款;調用任何transfer
時要求支付費用;要求transfer
參數_to
等於msg.sender
;要求transfer
參數_to
是NFT授權(approved
)的地址 - 僅讀的NFT註冊表,使用函數
unsafeTransfer
、transferFrom
、approve
以及setApprovalForAll
時都拋出異常。异常。