ERC-721 标准被提出用于 NFT(Non-Fungible Tokens)。 NFT 是指唯一的代币,这意味着智能合约中的每个代币都将与其他代币不同。它可以用来识别独特的东西,例如,某种图像、彩票、收藏品、绘画甚至音乐等。提醒你,ERC-20 是用于可替代代币的,如果你还没有读过关于 ERC-20 的文章,然后考虑先阅读它。 ERC-721 示例的最佳真实项目是Cryptokitties 。这是一款基于区块链的游戏,快来看看这款神奇的游戏,在乐趣中获取知识。
但这是如何工作的呢?这些图像、音乐和绘画如何存储在智能合约中?
好吧,这些东西并没有存储在合约中,而合约只是指向一个外部资产。有一个数字, tokenId
显示所有权。主要资产是通过 URI 附加到 tokenId 的图像或其他一些数据。让我们再深入一点。
我们为令牌 ID 分配一个 URI,它是一个 JSON 文本。该 JSON 文本包含图像或任何其他资产的详细信息和路径。并且用户只拥有tokenid
。例如,我的地址有tokenid
4 的余额,现在附加到该tokenid
的图像或任何类型的数据都将是我的。我拥有该数据,因为我拥有tokenid
。
现在我们必须将这两个东西,即图像和 JSON 文本 (URI) 存储在某个地方,以便任何 NFT 市场都可以获取图像并将其显示在他们的网站上。你可能会想为什么我们需要元数据 URI,为什么不直接将 Image URI 分配给 token id?好吧,你可能还记得为什么我们首先需要 ERC 标准,是的,这样客户端应用程序就可以很容易地与合约连接。同样,我们为 NFT 编写的元数据应采用 NFT 市场推荐的适当格式,以便他们可以轻松地从该 JSON 文件中获取数据并在其网站上显示数据。
现在让我们看看存储的东西。
图片和URI的存储方式有两种。我们可以将其托管在链上(区块链内)或链下(区块链外)。
链上数据存储在区块链中,但它占用了大量空间,这是负担不起的。想想在部署合同时支付数千美元。听起来不太好。除此之外,区块链也提供了有限的存储空间,你不能在其中存储大文件。
所以我们可以选择在区块链之外托管数据。通过我们的网站使用 AWS 或任何其他云服务。但这扼杀了区块链的主要思想,即去中心化。如果服务器崩溃或有人入侵怎么办?因为会有一个服务器托管所有数据。因此,我们需要其他能够抵御攻击且分散的东西,这就是 IPFS(星际文件系统),它是一种分布式存储系统。 IPFS 有多个节点,类似于存储数据的区块链的想法。但您无需支付任何费用。我们可以在 IPFS 上托管我们的图像和 JSON 文件 (URI),这样做将提供一个直接指向数据的唯一 CID(随机乱码),我们将特定的 CID 分配给特定的令牌 ID。稍后我们将详细讨论技术部分,首先让我们了解 ERC-721 标准的接口。
让我们看看 ERC-721 接口是什么样子的。
该标准具有以下功能:
将代币从一个账户转移到另一个账户。
获取帐户的当前代币余额。
获取特定令牌的所有者。
网络上可用的代币总供应量。
除此之外,它还有一些其他功能,比如批准第三方账户可以从一个账户转移一定数量的代币。
现在让我们看一下 ERC-721 的接口。
pragma solidity ^0.4.20; interface ERC721 { event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); function balanceOf(address _owner) external view returns (uint256); function ownerOf(uint256 _tokenId) external view returns (address); function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; function transferFrom(address _from, address _to, uint256 _tokenId) external payable; function approve(address _approved, uint256 _tokenId) external payable; function setApprovalForAll(address _operator, bool _approved) external; function getApproved(uint256 _tokenId) external view returns (address); function isApprovedForAll(address _owner, address _operator) external view returns (bool); } Break
function balanceOf(address _owner) external view returns (uint256);
2. ownerOf寻找一个NFT的拥有者。现在,这个函数告诉我们特定 NFT 的所有者。我们保留与上述类似的数据。映射(uint => 地址)公共余额;分配给零地址的 NFT 被认为是无效的,对它们的查询应该会抛出错误。
function ownerOf(uint256 _tokenId) external view returns (address);
3. safeTransferFrom将 NFT 的所有权从一个地址转移到另一个地址。它应该只将 tokenId 的所有者设置为新地址,并更新balances
映射。它应该在以下情况下抛出错误:
msg.sender
是该 NFT 的当前所有者、授权操作员或批准的地址_from
不是当前所有者__to_
是零地址。__tokenId_
不是有效的 NFT
转账完成后,此函数会检查__to_
是否为智能合约(代码大小 > 0)。如果是这样,它会在__to_
上调用onERC721Received
函数,如果返回值不等于此值则抛出: bytes4(keccak256("onERC721Received(address, address,uint256, bytes)")).
它以前如何?这就是为什么该函数被称为安全传输的原因,我们稍后会详细介绍。
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
4. safeTransferFrom :除了这个函数只是将数据设置为“”之外,它的工作方式与上面的函数相同,但有一个额外的数据参数。
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
这两个函数如何具有相同的名称?
有趣的事实:与 Javascript 不同,Solidity 确实支持函数重载。意味着我们可以定义两个具有相同名称的函数,唯一的条件是参数应该不同,从而使函数签名有所不同。不知道函数签名是什么?干得好。 答案链接
5. transferFrom转移 NFT 的所有权。与safeTransferFrom
函数的工作方式相同,唯一的区别是它不会对接收者调用安全调用( onERC721Received
)。因此,产生了丢失 NFT 的风险。
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
6. 批准与ERC20批准功能相同的概念,但功能和逻辑更多。为了让这个函数起作用,我们定义了一个嵌套映射: mapping(uint =>address) internal allowance;
在此映射中,uint 指代币 ID 和获准对该特定代币 ID 执行操作的地址。此批准功能应检查msg.sender
是当前所有者还是令牌所有者的操作员。如果此检查通过,则只应更新映射。这里的操作员是什么?看下一个函数。
function approve(address _approved, uint256 _tokenId) external payable;
7. setApprovalForAll 的工作方式与之前的批准函数类似,但该函数不是批准单个令牌 ID 的地址,而是批准和处理特定地址拥有的所有令牌。这是什么意思?这意味着你可以为你的 NFT 创建一个地址运算符。在您撤销所有权之前,该地址将成为您 NFT 的所有者。这次我们再次使用映射: mapping(address => mapping(address=>bool))internal operator;
第一个地址为第二个地址分别设置布尔值 true 或 false 以批准或撤销。
function setApprovalForAll(address _operator, bool _approved) external;
8. getApproved类似于ERC-20接口中的allowance函数。返回单个 NFT 的批准address
。它检查我们在上面的 approve 函数中更新的映射。
function getApproved(uint256 _tokenId) external view returns (address);
9. isApprovedForAll类似于上面的函数。它查询一个address
是否是另一个address
的授权operator
。此函数不应返回特定tokenId
的批准address
,而应返回我们在setApprovalForAll
函数中更新的给定address
的operator
address
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
转移:当任何 NFT 的所有权通过任何机制发生变化时发出。当 NFT 创建(from == 0)
和销毁(to == 0).
例外:在合约创建期间,可以创建和分配任意数量的 NFT,而不会发出 Transfer。在任何转移时,该 NFT(如果有)的批准地址将重置为无。
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
当 NFT 的批准地址发生更改或重新确认时,批准就会发出。零地址表示没有批准的地址。当 Transfer 事件发出时,这也表明该 NFT(如果有)的批准地址被重置为无。
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
ApproveForAll在为所有者启用或禁用运算符时发出。运营者可以管理所有者的所有 NFT。
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
这是 ERC 721 合约的接口。现在让我们进一步了解它的实现和规则。
在实施之前,还有一件事。还记得我们在谈到转移代币时谈到过一个叫做onERC721Received
的函数吗?现在让我们谈谈这个。如果 ERC-721 代币的接收者是合约,那么它需要与 ERC-721 代币一起运行。因为如果没有与接收方合约中的代币交互的功能怎么办?代币将永远锁定在该合约中。由于没有功能,没有人可以将这些令牌带到任何地址。为了克服这个漏洞,ERC-721 代币的接收者应该实现接收者接口,否则没有合约可以向该合约发送任何 ERC-721 代币。让我们看一下接收器接口。
这个接口只包含一个函数来实现,那就是onERC721Received,这个函数应该处理 NFT 的接收。 ERC-721 智能合约在transfer
后调用接收方的这个函数。这个函数可以抛出以恢复并拒绝传输。除魔法值之外的返回必须导致事务被还原。
interface ERC721TokenReceiver { function onERC721Received(address _operator,address _from,uint256 _tokenId,bytes _data) external returns(bytes4); }
在这里你可能会想,代币合约如何知道接收方是否实现了ERC721TokenReceiver
接口?因此,在发送令牌之前,您必须先检查这一部分。为此,让我们看看 ERC-721 的openzeppelin实现。这是在发送令牌之前应该在safeTransfer()
内部调用的私有函数。如果这返回 true,那么只应该发生事务。
好吧,这并不能保证将 NFT 从接收方合约中取出的功能,因为它只是检查接收方功能的实现,而不是将 NFT 转移出合约的功能。那么它的意义何在?这个方法可以告诉我们,接收方合约的作者至少知道这个方法,这意味着他们必须实现将 NFT 转出合约的功能。当然,没有人会希望他们的代币卡在合约中。
上述接口对于 ERC-721 代币合约是强制性的。现在,一些接口在理论上是可选的,但使合约更清晰、更易用。
这些是ERC721Metadata和ERC721Enumerable。让我们一一抓住他们。
interface ERC721Metadata/* is ERC721 */ { /// @notice A descriptive name for a collection of NFTs in this /// contract function name()external view returns (string _name); /// @notice An abbreviated name for NFTs in this contract function symbol()external view returns (string _symbol); /// @notice A distinct Uniform Resource Identifier (URI) for a given /// asset. /// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined /// in RFC3986. The URI may point to a JSON file that conforms to /// the "ERC721Metadata JSON Schema". function tokenURI(uint256 _tokenId)external view returns (string);
ERC721Enumerable——这只是提供了有关 NFT 的数据。
interface ERC721Enumerable/* is ERC721 */ { /// @notice Count NFTs tracked by this contract /// @return A count of valid NFTs tracked by this contract, where /// each one of them has an assigned and queryable owner not equal /// to the zero address function totalSupply()external view returns (uint256); /// @notice Enumerate valid NFTs /// @dev Throws if `_index` >= `totalSupply()`. /// @param _index A counter less than `totalSupply()` /// @return The token identifier for the `_index`th NFT, /// (sort order not specified) function tokenByIndex(uint256 _index)external view returns (uint256); /// @notice Enumerate NFTs assigned to an owner /// @dev Throws if `_index` >= `balanceOf(_owner)` or if /// `_owner` is the zero address, representing invalid NFTs. /// @param _owner An address where we are interested in NFTs owned /// by them /// @param _index A counter less than `balanceOf(_owner)` /// @return The token identifier for the `_index`th NFT assigned to /// `_owner`, /// (sort order not specified) function tokenOfOwnerByIndex(address _owner,uint256 _index)external view returns(uint256);
现在,我们怎么知道一个接口是否已经被合约实现了呢?为此,我们需要跑题,即ERC-165,它有一个功能:
function supportsInterface(bytes4 interfaceID) external view returns (bool);
此函数将您要检查的 interfaceId 作为参数并将其与您要检查的 interfaceId 相匹配。我会告诉你什么是 interfaceId,请耐心等待。先看openzeppelin对这个功能的实现。
让我们更多地了解这个函数type(IERC721).interfaceId
中的interfaceId;
interfaceID是通过两个主要的操作来实现的。
1. keccak 256哈希
2.异或运算
keccak256 是一种接受输入并以字节为单位吐出一些随机字符串的算法。现在让我们找出这个函数的 keccak 哈希。
function supportsInterface(bytes4 interfaceID) external view returns (bool);
语法看起来像 ṭhis。
bytes4(keccak256('supportsInterface(bytes4)')
;
现在我们有了这个函数的 keccak 哈希。请记住,您不必在 keccak256 的参数内传递整个函数,只需传递签名即可。签名意味着只有名称和参数类型,甚至不包括参数的名称。在得到所有函数的 keccak256 哈希后,我们对它们进行异或运算,得到的结果就是 interfaceId。让我们看看如何。异或运算接受输入并在比较它们后给出一些输出。如果一个且只有一个输入为true
,则它输出真。如果两个输入均为假或均为真,则输出结果为false
。
您无需了解 XOR 背后的数学原理。请记住,您获取每个函数的keccak256
哈希并将它们传递给 XOR 门,您获得的值就是 interfaceID。所以主要的想法是获取函数的 keccak 哈希并获得它们的 XOR 输出。该输出是 interfaceId 之后,您可以检查合同是否具有相同的 interfaceID,如果是,则意味着该合同正在实现所需的接口。
别担心,坚固性让这对你来说很容易。下面以ERC721Metadata接口为例进行学习。
interface ERC721Metadata/* is ERC721 */ { function name()external view returns (string _name); function symbol()external view returns (string _symbol); function tokenURI(uint256 _tokenId)external view returns (string); }
这里我们有三个函数。所以我写了这段代码让你明白。注意这里的插入符号^
。该符号表示两个值之间的异或运算。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.16 ; interface metadata { function name()external view returns (string memory _name ); function symbol()external view returns (string memory _symbol); function tokenURI(uint256 _tokenId)external view returns (string memory a); } contract { //old and lengthy way by applying the keccak256 algorithm and performing XOR. function getHashOldWay ()public pure returns(bytes4){ return bytes4(keccak256('name()')) ^ bytes4(keccak256('symbol()'))^ bytes4(keccak256('tokenURI(uint256)')); } //Improved way function getHashNewWay ()public pure returns(bytes4){ metadata a; return a.name.selector ^ a.symbol.selector ^ a.tokenURI.selector; } //this is the latest simplest way to get the interfaceId function getHashLatestWay ()public pure returns(bytes4){ return type(metadata).interfaceId; } }
哦,我们对 ERC165 的了解太多了,以至于我们忘记了 ERC721 让我们回到它。
现在是了解代币 URI 的正确时机,它可以帮助各种 NFT 市场识别您分配给特定代币 ID 的数据。由于 Opensea 是最著名的市场,我们将以它为例。 Opensea 期望您的 ERC721 合约通过调用tokenURI()
函数返回 tokenURI。让我们看一下 openzeppelin 合约。
1. 所有令牌的基本 URI 相同,因 tokenId 而异。
通过这种方式,我们为所有令牌分配一个基本 URI,然后将 tokenId 连接到它,获取元数据 URI。这只有在您以每个元数据 JSON 文件的链接都相同的方式分配集合时才有可能,唯一的区别是 tokenId。例如,
https://gateway.pinata.cloud/ipfs/QmYLwrqMmzC3k4eZu7qJ4MZJ4SNYMgqbRJFLkyiPtUBZUP/这个链接指向文件夹,意味着这是baseURI。
要获取单个元数据,我们需要转到https://gateway.pinata.cloud/ipfs/QmYLwrqMmzC3k4eZu7qJ4MZJ4SNYMgqbRJFLkyiPtUBZUP/1.json
现在您必须以一种直接转到该特定令牌的元数据的方式从合同返回 tokenURI,并且市场可以获取它。我们通过将 baseURI 与 tokenId 连接起来并对其进行 abi 编码来做到这一点。看看下面这个函数。
function tokenURI(uint tokenId) override public view returns(string memory) { return (string(abi.encodePacked( "https://gateway.pinata.cloud/ipfs/QmYLwrqMmzC3k4eZu7qJ4MZJ4SNYMgqbRJFLkyiPtUBZUP/",Strings.toString(tokenId),".json")) ); }
市场将调用此函数来获取 URI 并显示我们在 JSON 文件中提供的所有数据。
2. 每个令牌的不同 URI。
这是我们可以分配位于不同位置的 URI 的另一种方式。单个文件的 IPFS 链接如下所示。 https://ipfs.filebase.io/ipfs/Qma65D75em77UTgP5TYXT4ZK5Scwv9ZaNyJPdX9DNCXRWc
这样你就不能仅仅将 tokenId 与 baseURI 连接来获得这个链接,相反,这个链接应该直接附加到链上的 tokenId。为此,请查看上面的ERC721URIStorage
合约,我们在其中定义了一个私有映射以将 URI 分配给特定的 tokenId。稍后还定义了一个函数,该函数使用给定的 URI 填充映射。 tokenURI
函数通过添加返回映射 URI 的新功能来覆盖父函数。
需要注意的另一件事是 JSON 模式的格式。 JSON 应以标准化方式包含数据,以便市场可以获取所需数据并显示这些数据。
ERC721 的标准 JSON 模式。
{ "title": "Asset Metadata", "type": "object", "properties": { "name": { "type": "string", "description": "Identifies the asset to which this NFT represents" }, "description": { "type": "string", "description": "Describes the asset to which this NFT represents" }, "image": { "type": "string", "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive." } } }
如果您对 ERC-721 (NFT) 智能合约开发感兴趣,那么这些是您必须知道的事情。
问题:
ERC721 标准在可扩展性方面存在几个问题。转移代币时会出现问题,因为 ERC-721 允许您一次转移一个代币。这意味着如果你想向某人发送 5 个 NFT,那么你将需要执行 5 个交易。你很清楚它在区块链中需要多少空间。这就是为什么在牛市运行中网络面临问题的原因,因为网络中的 rush 太多,gas 费用迅速增加。
ERC-1155 解决了这些问题。如何?让我们在单独的帖子中讨论这个。如果你已经完全理解了 ERC721 标准,那么 ERC1155 理解起来也不难。
也发布在这里。