本文是Token Standards文章系列的第11部分,介绍了以太坊的ERC-721标准,用于NFT(非同质化代币)。ERC-721代币是唯一的,可以用来标识独特的资产。文章详细解释了ERC-721接口的功能,包括代币转账、余额查询、所有者查询等,以及如何存储NFT的图像和元数据,并讨论了ERC-721标准的局限性。
欢迎来到 Token Standards 系列文章的第 11 部分。如果你还没有看过第一部分,请查看 这里 。
ERC-721 标准是为 NFT(Non-Fungible Tokens,非同质化代币)提出的。NFT 指的是独一无二的代币,这意味着智能合约中的每个代币都与其他代币不同。它可以用来识别独特的事物,例如,某种图像、彩票、收藏品、绘画,甚至音乐等。
提醒你,ERC-20 用于同质化代币,如果你还没有阅读关于 ERC-20 的文章,请考虑先阅读它。ERC-721 的最佳真实项目案例是 Cryptokitties。这是一款基于区块链的游戏,请查看这款精彩的游戏,在娱乐中学习知识。
但是这是如何工作的呢?这些图像、音乐和绘画是如何存储在智能合约中的呢?
实际上,这些东西并没有存储在合约内部,而是合约只是指向一个外部资产。有一个数字 tokenId
显示所有权。主要资产是图像或其他一些数据,这些数据通过 URI 附加到 tokenId。让我们更深入地了解一下。
我们将一个 URI(JSON 文本)分配给代币 ID。JSON 文本包含图像或任何其他资产的详细信息和路径。用户只拥有 tokenid
。例如,我的地址的 tokenid
余额为 4,那么附加到该 tokenid
的图像或任何类型的数据都将属于我。我拥有该数据,因为我拥有 tokenid
。
现在我们必须将这两件事(即图像和 JSON 文本 (URI))存储在某个地方,以便任何 NFT 市场都可以获取图像并在其网站上显示它。你可能在想,为什么我们需要元数据 URI,为什么不直接将图像 URI 分配给代币 ID 呢?你可能还记得我们最初为什么需要 ERC 标准,是的,这样客户端应用程序就可以轻松地与合约连接。
同样,我们为 NFT 编写的元数据应该采用 NFT 市场推荐的正确格式,以便他们可以轻松地从该 JSON 文件中获取数据并在其网站上显示数据。
现在让我们看看存储部分。
有两种方法可以存储图像和 URI。
我们可以将其托管在链上(区块链内部)或链下(区块链外部)。
链上数据存储在区块链中,但会占用大量空间,这是负担不起的。想象一下在部署合约时支付数千美元。听起来不太好。除此之外,区块链提供的存储空间也有限,你无法在其中存储大型文件。
因此,我们可以选择在区块链之外托管数据。通过我们的网站使用 AWS 或任何其他云服务。但这扼杀了区块链的主要思想,即去中心化。如果服务器崩溃或有人入侵怎么办?因为会有一个服务器托管所有数据。
因此,我们需要其他既能抵抗攻击又是去中心化的东西,那就是 IPFS(Inter Planetary File System,星际文件系统),它是一个分布式存储系统。IPFS 具有多个节点,类似于区块链存储数据的概念。但你不必支付任何费用。
我们可以将我们的图像和 JSON 文件 (URI) 托管在 IPFS 上,这样做会给出一个唯一的 CID(随机乱码),该 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);
}
1. balanceOf 返回所有者账户中的代币数量。我们如何跟踪余额?实际上,它总是一样的,我们定义一个映射来跟踪钱包中的代币总数。mapping(address => uint) public balances;
记住,它只是将代币总数相加并返回相同的值,这个映射不知道代币的所有者,因为它只是告诉一个地址拥有的代币 ID 的数量。
function balanceOf(address _owner) external view returns (uint256);
2. ownerOf 查找 NFT 的所有者。现在,这个函数告诉我们特定 NFT 的所有者。我们以类似于上述方式保存此数据。mapping( uint => address) public balances;
分配给零地址的 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. approve 与 ERC20 批准功能的概念相同,但具有更多功能和逻辑。为了使这个函数起作用,我们定义了一个嵌套的映射:mapping(uint =>address) internal allowance;
在此映射中,uint 指的是代币 ID,地址是被批准对该特定 tokenId 执行操作的地址。
此批准函数应检查 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);
Transfer: 当任何 NFT 的所有权通过任何机制更改时发出。此事件在创建 NFT (from == 0)
和销毁 (to == 0)
时发出。
例外: 在合约创建期间,可以创建和分配任意数量的 NFT,而无需发出 Transfer 事件。在任何转移时,该 NFT 的批准地址(如果有)都会重置为 none。
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
Approval 当 NFT 的批准地址被更改或确认时发出。零地址表示没有批准的地址。当发出 Transfer 事件时,这也表示该 NFT 的批准地址(如果有)已重置为 none。
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 代币的接收者应实现接收者 Interface,否则任何合约都无法将任何 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。 让我们逐一了解它们。
ERC721 Metadata: 元数据扩展主要用于查询合约以获取代币名称。现在让我们看一下元数据接口。这很容易理解。
interface ERC721Metadata/* is ERC721 */ {
/// @notice 此合约中 NFT 集合的描述性名称
function name()external view returns (string _name);
/// @notice 此合约中 NFT 的缩写名称
function symbol()external view returns (string _symbol);
/// @notice 给定资产的不同统一资源标识符 (URI)。
/// @dev 如果 `_tokenId` 不是有效的 NFT,则抛出。URI 在 RFC3986 中定义。URI 可能指向符合
/// “ERC721Metadata JSON Schema”的 JSON 文件。
function tokenURI(uint256 _tokenId)external view returns (string);
ERC721Enumerable 这只是给出了关于 NFT 的数据。
interface ERC721Enumerable/* is ERC721 */ {
/// @notice 统计此合约跟踪的 NFT
/// @return 此合约跟踪的有效 NFT 的计数,其中
/// 它们中的每一个都具有已分配且可查询的所有者,该所有者不等于
/// 零地址
function totalSupply()external view returns (uint256);
/// @notice 枚举有效的 NFT
/// @dev 如果 `_index` >= `totalSupply()`,则抛出。
/// @param _index 小于 `totalSupply()` 的计数器
/// @return 第 `_index` 个 NFT 的代币标识符,
/// (未指定排序顺序)
function tokenByIndex(uint256 _index)external view returns (uint256);
/// @notice 枚举分配给所有者的 NFT
/// @dev 如果 `_index` >= `balanceOf(_owner)` 或如果
/// `_owner` 是零地址,表示无效的 NFT,则抛出。
/// @param _owner 我们对他们拥有的 NFT 感兴趣的地址
/// @param _index 小于 `balanceOf(_owner)` 的计数器
/// @return 分配给 `_owner` 的第 `_index` 个 NFT 的代币标识符,
/// (未指定排序顺序)
function tokenOfOwnerByIndex(address _owner,uint256 _index)external view returns(uint256);
现在,我们如何才能知道合约是否实现了接口?为此,我们需要离题一下,即 ERC-165, 它有一个函数:
function supportsInterface(bytes4 interfaceID) external view returns (bool);
此函数将你要检查的 interfaceId 作为参数,并将其与你要检查的 interfaceId 匹配。我会告诉你什么是 interfaceId,请耐心等待。首先,看一下 openzeppelin 对此函数的实现。
让我们更多地了解此函数中的 interfaceId type(IERC721).interfaceId
;
interfaceID 通过两个主要操作实现。
1. keccak256 哈希
2. XOR 运算
keccak256 是一种算法,它接受输入并吐出一些字节中的随机字符串。我们取函数名称和参数的 keccak256 的前 4 个字节,并将其称为函数选择器。函数选择器是 solidity 的一个非常重要的部分。计算接口 ID 只是我们使用它的一小部分操作。
现在让我们找出此函数的选择器。
function supportsInterface(bytes4 interfaceID) external view returns (bool);
语法看起来像这样。 bytes4(keccak256('supportsInterface(bytes4)'));
现在我们有了此函数的选择器哈希。请记住,你不必将整个函数传递到 keccak256 的参数中,只需传递选择器即可。选择器仅表示名称和参数类型,甚至不包括参数的名称。
在获得所有函数的选择器之后,我们在它们之间执行 XOR 运算,我们获得的结果就是 interfaceId。让我们看看如何操作。
XOR 运算接受输入并在比较后给出一些输出。如果只有一个输入为真,则输出 true
。如果两个输入均为假或两个输入均为真,则输出结果为 false
。
你不需要了解 XOR 背后的数学原理。只需记住,你获取每个函数的选择器并将它们传递到 XOR 门,获得的值就是 interfaceID。因此,主要思想是获取函数的选择器并获得它们的 XOR 输出。该输出是 interfaceId,之后,你可以检查合约是否具有相同的 interfaceID,如果是,则表示合约正在实现所需的接口。
不用担心,solidity 已经让这变得容易了。让我们以 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);
}
在这里我们有三个函数。因此,我编写了这段代码来让你理解。
请注意此处插入符号 ^
。此符号表示两个值之间的 XOR 运算。
// 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 {
//使用 keccak256 算法并执行 XOR 的旧方法和冗长的方法。
function getHashOldWay ()public pure returns(bytes4){
return bytes4(keccak256('name()')) ^ bytes4(keccak256('symbol()'))^ bytes4(keccak256('tokenURI(uint256)'));
}
//改进的方法
function getHashNewWay ()public pure returns(bytes4){
metadata a;
return a.name.selector ^ a.symbol.selector ^ a.tokenURI.selector;
}
//这是获取 interfaceId 的最新最简单的方法
function getHashLatestWay ()public pure returns(bytes4){
return type(metadata).interfaceId;
}
}
表示
哦,我们对 ERC165 了解太多了,以至于忘记了 ERC721,让我们回到它。
现在是了解 token URI 的正确时机,它可以帮助各种 NFT 市场识别你使用特定 tokenId 分配的数据。由于 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."
}
}
}
注意事项:
ERC721 标准在可扩展性方面存在几个问题。
问题出现在转移代币时,因为 ERC-721 允许你一次转移一个代币。
这意味着如果你想向某人发送 5 个 NFT,则需要执行 5 个交易。你很清楚这会在区块链中占用多少空间。这就是在牛市中网络面临问题的原因,因为网络上的拥堵过多,Gas 费迅速上涨。
ERC-1155 解决了这些问题。如何解决?让我们在另一篇文章中讨论这个问题。
如果你已经完全理解了 ERC721 标准,那么 ERC1155 将不会很难理解。
- 原文链接: decipherclub.com/ethereu...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!