# ERC721标准 **Published by:** [paedy](https://paragraph.com/@paedy/) **Published on:** 2021-11-20 **URL:** https://paragraph.com/@paedy/erc721 ## Content 一、代币代币让我们来聊聊以太坊上的代币。 如果你对以太坊的世界有一些了解,你很可能听过人们聊到代币——尤其是 ERC20 代币。 一个 代币 在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了所有其他代币合约共享的一组标准函数,例如 transfer(address _to, uint256 _value) 和 balanceOf(address _owner). 在智能合约内部,通常有一个映射, mapping(address => uint256) balances,用于追踪每个地址还有多少余额。 所以基本上一个代币只是一个追踪谁拥有多少该代币的合约,和一些可以让那些用户将他们的代币转移到其他地址的函数。它为什么重要呢?由于所有 ERC20 代币共享具有相同名称的同一组函数,它们都可以以相同的方式进行交互。 这意味着如果你构建的应用程序能够与一个 ERC20 代币进行交互,那么它就也能够与任何 ERC20 代币进行交互。 这样一来,将来你就可以轻松地将更多的代币添加到你的应用中,而无需进行自定义编码。 你可以简单地插入新的代币合约地址,然后哗啦,你的应用程序有另一个它可以使用的代币了。 其中一个例子就是交易所。 当交易所添加一个新的 ERC20 代币时,实际上它只需要添加与之对话的另一个智能合约。 用户可以让那个合约将代币发送到交易所的钱包地址,然后交易所可以让合约在用户要求取款时将代币发送回给他们。 交易所只需要实现这种转移逻辑一次,然后当它想要添加一个新的 ERC20 代币时,只需将新的合约地址添加到它的数据库即可。其他代币标准对于像货币一样的代币来说,ERC20 代币非常酷。 但是要在我们僵尸游戏中代表僵尸就并不是特别有用。 首先,僵尸不像货币可以分割 —— 我可以发给你 0.237 以太,但是转移给你 0.237 的僵尸听起来就有些搞笑。 其次,并不是所有僵尸都是平等的。 你的2级僵尸"Steve"完全不能等同于我732级的僵尸"H4XF13LD MORRIS"。(你差得远呢,Steve)。 有另一个代币标准更适合如 CryptoZombies 这样的加密收藏品——它们被称为ERC721 代币. ERC721代币是不能互换的,因为每个代币都被认为是唯一且不可分割的。 你只能以整个单位交易它们,并且每个单位都有唯一的 ID。 这些特性正好让我们的僵尸可以用来交易。请注意,使用像 ERC721 这样的标准的优势就是,我们不必在我们的合约中实现拍卖或托管逻辑,这决定了玩家能够如何交易/出售我们的僵尸。 如果我们符合规范,其他人可以为加密可交易的 ERC721 资产搭建一个交易所平台,我们的 ERC721 僵尸将可以在该平台上使用。 所以使用代币标准相较于使用你自己的交易逻辑有明显的好处。实战演练我们将在下一章深入讨论ERC721的实现。 但首先,让我们为本课设置我们的文件结构。 我们将把所有ERC721逻辑存储在一个叫ZombieOwnership的合约中。1、在文件顶部声明我们pragma的版本(格式参考之前的课程)。2、将 zombieattack.sol import 进来。3、声明一个继承 ZombieAttack 的新合约, 命名为ZombieOwnership。合约的其他部分先留空。zombieownership.sol// 从这里开始 pragma solidity ^0.4.19; import "./zombieattack.sol"; contract ZombieOwnership is ZombieAttack { } 二、ERC721标准与多重继承让我们来看一看 ERC721 标准:contract ERC721 { event Transfer(address indexed _from, address indexed _to, uint256 _tokenId); event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId); function balanceOf(address _owner) public view returns (uint256 _balance); function ownerOf(uint256 _tokenId) public view returns (address _owner); function transfer(address _to, uint256 _tokenId) public; function approve(address _to, uint256 _tokenId) public; function takeOwnership(uint256 _tokenId) public; } 这是我们需要实现的方法列表,我们将在接下来的章节中逐个学习。 虽然看起来很多,但不要被吓到了!我们在这里就是准备带着你一步一步了解它们的。注意: ERC721目前是一个 草稿,还没有正式商定的实现。在本教程中,我们使用的是 OpenZeppelin 库中的当前版本,但在未来正式发布之前它可能会有更改。 所以把这 一个 可能的实现当作考虑,但不要把它作为 ERC721 代币的官方标准。实现一个代币合约在实现一个代币合约的时候,我们首先要做的是将接口复制到它自己的 Solidity 文件并导入它,import ./erc721.sol。 接着,让我们的合约继承它,然后我们用一个函数定义来重写每个方法。 但等一下—— ZombieOwnership 已经继承自 ZombieAttack 了 —— 它如何能够也继承于 ERC721 呢? 幸运的是在Solidity,你的合约可以继承自多个合约,参考如下:contract SatoshiNakamoto is NickSzabo, HalFinney { // 啧啧啧,宇宙的奥秘泄露了 } 正如你所见,当使用多重继承的时候,你只需要用逗号 , 来隔开几个你想要继承的合约。在上面的例子中,我们的合约继承自 NickSzabo 和 HalFinney。 来试试吧。实战演练我们已经在上面为你创建了带着接口的 erc721.sol 。1、将 erc721.sol 导入到 zombieownership.sol2、声明 ZombieOwnership 继承自 ZombieAttack 和 ERC721zombieownership.solpragma solidity ^0.4.19; import "./zombieattack.sol"; // 在这里引入文件 import "./erc721.sol"; // 在这里声明 ERC721 的继承 contract ZombieOwnership is ZombieAttack, ERC721 { } 三、 balanceOf和ownerOf现在,我们来深入讨论一下 ERC721 的实现。 我们已经把所有你需要在本课中实现的函数的空壳复制好了。 在本章节,我们将实现头两个方法: balanceOf 和 ownerOf。balanceOffunction balanceOf(address _owner) public view returns (uint256 _balance); 这个函数只需要一个传入 address 参数,然后返回这个 address 拥有多少代币。 在我们的例子中,我们的“代币”是僵尸。你还记得在我们 DApp 的哪里存储了一个主人拥有多少只僵尸吗?ownerOffunction ownerOf(uint256 _tokenId) public view returns (address _owner); 这个函数需要传入一个代币 ID 作为参数 (我们的情况就是一个僵尸 ID),然后返回该代币拥有者的 address。 同样的,因为在我们的 DApp 里已经有一个 mapping (映射) 存储了这个信息,所以对我们来说这个实现非常直接清晰。我们可以只用一行 return 语句来实现这个函数。注意:要记得, uint256 等同于uint。我们从课程的开始一直在代码中使用 uint,但从现在开始我们将在这里用 uint256,因为我们直接从规范中复制粘贴。实战演练我将让你来决定如何实现这两个函数。 每个函数的代码都应该只有1行 return 语句。看看我们在之前课程中写的代码,想想我们都把这个数据存储在哪。如果你觉得有困难,你可以点“我要看答案”的按钮来获得帮助。1、实现 balanceOf 来返回 _owner 拥有的僵尸数量。2、实现 ownerOf 来返回拥有 ID 为 _tokenId 僵尸的所有者的地址。zombieownership.solpragma solidity ^0.4.19; import "./zombieattack.sol"; import "./erc721.sol"; contract ZombieOwnership is ZombieAttack, ERC721 { function balanceOf(address _owner) public view returns (uint256 _balance) { // 1\. 在这里返回 `_owner` 拥有的僵尸数 return ownerZombieCount[_owner]; } function ownerOf(uint256 _tokenId) public view returns (address _owner) { // 2\. 在这里返回 `_tokenId` 的所有者 return zombieToOwner[_tokenId]; } function transfer(address _to, uint256 _tokenId) public { } function approve(address _to, uint256 _tokenId) public { } function takeOwnership(uint256 _tokenId) public { } } 四、重构Hey!我们刚刚的代码中其实有个错误,以至于其根本无法通过编译,你发现了没? 在前一个章节我们定义了一个叫 ownerOf 的函数。但如果你还记得第4课的内容,我们同样在zombiefeeding.sol 里以 ownerOf 命名创建了一个 modifier(修饰符)。 如果你尝试编译这段代码,编译器会给你一个错误说你不能有相同名称的修饰符和函数。 所以我们应该把在 ZombieOwnership 里的函数名称改成别的吗? 不,我们不能那样做!!!要记得,我们正在用 ERC721 代币标准,意味着其他合约将期望我们的合约以这些确切的名称来定义函数。这就是这些标准实用的原因——如果另一个合约知道我们的合约符合 ERC721 标准,它可以直接与我们交互,而无需了解任何关于我们内部如何实现的细节。 所以,那意味着我们将必须重构我们第4课中的代码,将 modifier 的名称换成别的。实战演练我们回到了 zombiefeeding.sol 。我们将把 modifier 的名称从 ownerOf 改成 onlyOwnerOf。1、把修饰符定义中的名称改成 onlyOwnerOf2、往下滑到使用此修饰符的函数 feedAndMultiply 。我们也需要改这里的名称。注意:我们在 zombiehelper.sol 和 zombieattack.sol 里也使用了这个修饰符,所以这两个文件也必须把名字改了。zombiefeeding.solpragma solidity ^0.4.19; import "./zombiefactory.sol"; contract KittyInterface { function getKitty(uint256 _id) external view returns ( bool isGestating, bool isReady, uint256 cooldownIndex, uint256 nextActionAt, uint256 siringWithId, uint256 birthTime, uint256 matronId, uint256 sireId, uint256 generation, uint256 genes ); } contract ZombieFeeding is ZombieFactory { KittyInterface kittyContract; // 1\. 把修饰符名称改成 `onlyOwnerOf` modifier onlyOwnerOf(uint _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); _; } function setKittyContractAddress(address _address) external onlyOwner { kittyContract = KittyInterface(_address); } function _triggerCooldown(Zombie storage _zombie) internal { _zombie.readyTime = uint32(now + cooldownTime); } function _isReady(Zombie storage _zombie) internal view returns (bool) { return (_zombie.readyTime <= now); } // 2\. 这里也要修改修饰符的名称 function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal onlyOwnerOf(_zombieId) { Zombie storage myZombie = zombies[_zombieId]; require(_isReady(myZombie)); _targetDna = _targetDna % dnaModulus; uint newDna = (myZombie.dna + _targetDna) / 2; if (keccak256(_species) == keccak256("kitty")) { newDna = newDna - newDna % 100 + 99; } _createZombie("NoName", newDna); _triggerCooldown(myZombie); } function feedOnKitty(uint _zombieId, uint _kittyId) public { uint kittyDna; (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId); feedAndMultiply(_zombieId, kittyDna, "kitty"); } } 五、ERC721转移标准现在我们将通过学习把所有权从一个人转移给另一个人来继续我们的 ERC721 规范的实现。 注意 ERC721 规范有两种不同的方法来转移代币:function transfer(address _to, uint256 _tokenId) public; function approve(address _to, uint256 _tokenId) public; function takeOwnership(uint256 _tokenId) public; 1、第一种方法是代币的拥有者调用transfer 方法,传入他想转移到的 address 和他想转移的代币的 _tokenId。2、第二种方法是代币拥有者首先调用 approve,然后传入与以上相同的参数。接着,该合约会存储谁被允许提取代币,通常存储到一个 mapping (uint256 => address) 里。然后,当有人调用 takeOwnership 时,合约会检查 msg.sender 是否得到拥有者的批准来提取代币,如果是,则将代币转移给他。你注意到了吗,transfer 和 takeOwnership 都将包含相同的转移逻辑,只是以相反的顺序。 (一种情况是代币的发送者调用函数;另一种情况是代币的接收者调用它)。所以我们把这个逻辑抽象成它自己的私有函数 _transfer,然后由这两个函数来调用它。 这样我们就不用写重复的代码了。实战演练让我们来定义 _transfer 的逻辑。1、定义一个名为 _transfer的函数。它会需要3个参数:address _from、address _to和uint256 _tokenId。它应该是一个 私有 函数。2、我们有2个映射会在所有权改变的时候改变: ownerZombieCount (记录一个所有者有多少只僵尸)和 zombieToOwner (记录什么人拥有什么)。我们的函数需要做的第一件事是为 接收 僵尸的人(address _to)增 加ownerZombieCount。使用 ++ 来增加。3、接下来,我们将需要为 发送 僵尸的人(address _from)减少ownerZombieCount。使用 -- 来扣减。4、最后,我们将改变这个 _tokenId 的 zombieToOwner 映射,这样它现在就会指向 _to。5、骗你的,那不是最后一步。我们还需要再做一件事情。ERC721规范包含了一个 Transfer 事件。这个函数的最后一行应该用正确的参数触发Transfer ——查看 erc721.sol 看它期望传入的参数并在这里实现。 zombieownership.zolpragma solidity ^0.4.19; import "./zombieattack.sol"; import "./erc721.sol"; contract ZombieOwnership is ZombieAttack, ERC721 { function balanceOf(address _owner) public view returns (uint256 _balance) { return ownerZombieCount[_owner]; } function ownerOf(uint256 _tokenId) public view returns (address _owner) { return zombieToOwner[_tokenId]; } // 在这里定义 _transfer() function _transfer(address _from, address _to, uint256 _tokenId) private { /*错误的写法 balanceOf(_to)++; balanceOf(_from)--; ownerOf(_tokenId); */ ownerZombieCount[_to]++; ownerZombieCount[_from]--; zombieToOwner[_tokenId] = _to; Transfer(_from, _to, _tokenId); } function transfer(address _to, uint256 _tokenId) public { } function approve(address _to, uint256 _tokenId) public { } function takeOwnership(uint256 _tokenId) public { } } 刚才那是最难的部分——现在实现公共的 transfer 函数应该十分容易,因为我们的 _transfer 函数几乎已经把所有的重活都干完了。实战演练1、我们想确保只有代币或僵尸的所有者可以转移它。还记得我们如何限制只有所有者才能访问某个功能吗?没错,我们已经有一个修饰符能够完成这个任务了。所以将修饰符 onlyOwnerOf 添加到这个函数中。2、现在该函数的正文只需要一行代码。它只需要调用 _transfer。记得把 msg.sender 作为参数传递进 address _from。zombieownership.zolpragma solidity ^0.4.19; import "./zombieattack.sol"; import "./erc721.sol"; contract ZombieOwnership is ZombieAttack, ERC721 { function balanceOf(address _owner) public view returns (uint256 _balance) { return ownerZombieCount[_owner]; } function ownerOf(uint256 _tokenId) public view returns (address _owner) { return zombieToOwner[_tokenId]; } function _transfer(address _from, address _to, uint256 _tokenId) private { ownerZombieCount[_to]++; ownerZombieCount[_from]--; zombieToOwner[_tokenId] = _to; Transfer(_from, _to, _tokenId); } // 1\. 在这里添加修饰符 function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) { // 2\. 在这里定义方法 _transfer(msg.sender, _to, _tokenId); } function approve(address _to, uint256 _tokenId) public { } function takeOwnership(uint256 _tokenId) public { } } 六、ERC721之批准现在,让我们来实现 approve。 记住,使用 approve 或者 takeOwnership 的时候,转移有2个步骤:1、你,作为所有者,用新主人的 address 和你希望他获取的 _tokenId 来调用 approve2、新主人用 _tokenId 来调用 takeOwnership,合约会检查确保他获得了批准,然后把代币转移给他。因为这发生在2个函数的调用中,所以在函数调用之间,我们需要一个数据结构来存储什么人被批准获取什么。实战演练1、首先,让我们来定义一个映射 zombieApprovals。它应该将一个 uint 映射到一个 address。这样一来,当有人用一个 _tokenId 调用 takeOwnership 时,我们可以用这个映射来快速查找谁被批准获取那个代币。2、在函数 approve 上, 我们想要确保只有代币所有者可以批准某人来获取代币。所以我们需要添加修饰符 onlyOwnerOf 到 approve。3、函数的正文部分,将 _tokenId 的 zombieApprovals 设置为和 _to 相等。4、最后,在 ERC721 规范里有一个 Approval 事件。所以我们应该在这个函数的最后触发这个事件。(参考 erc721.sol 来确认传入的参数,并确保 _owner 是 msg.sender)zombieownership.zolpragma solidity ^0.4.19; import "./zombieattack.sol"; import "./erc721.sol"; contract ZombieOwnership is ZombieAttack, ERC721 { // 1\. 在这里定义映射 mapping (uint => address) zombieApprovals; function balanceOf(address _owner) public view returns (uint256 _balance) { return ownerZombieCount[_owner]; } function ownerOf(uint256 _tokenId) public view returns (address _owner) { return zombieToOwner[_tokenId]; } function _transfer(address _from, address _to, uint256 _tokenId) private { ownerZombieCount[_to]++; ownerZombieCount[_from]--; zombieToOwner[_tokenId] = _to; Transfer(_from, _to, _tokenId); } function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) { _transfer(msg.sender, _to, _tokenId); } // 2\. 在这里添加方法修饰符 function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) { // 3\. 在这里定义方法 zombieApprovals[_tokenId] = _to; Approval(msg.sender, _to, _tokenId); // 协议事件 } function takeOwnership(uint256 _tokenId) public { } } 七、ERC721之takeOwnership现在让我们完成最后一个函数来结束 ERC721 的实现。 最后一个函数 takeOwnership, 应该只是简单地检查以确保 msg.sender 已经被批准来提取这个代币或者僵尸。若确认,就调用 _transfer;实战演练1、首先,我们要用一个 require 句式来检查 _tokenId 的 zombieApprovals 和 msg.sender 相等。这样如果 msg.sender 未被授权来提取这个代币,将抛出一个错误。2、为了调用 _transfer,我们需要知道代币所有者的地址(它需要一个 _from 来作为参数)。幸运的是我们可以在我们的 ownerOf 函数中来找到这个参数。所以,定义一个名为 owner 的 address 变量,并使其等于 ownerOf(_tokenId)。3、最后,调用 _transfer, 并传入所有必须的参数。(在这里你可以用 msg.sender 作为 _to, 因为代币正是要发送给调用这个函数的人)。注意: 我们完全可以用一行代码来实现第2、3两步。但是分开写会让代码更易读。一点个人建议 :)zombieownership.zolpragma solidity ^0.4.19; import "./zombieattack.sol"; import "./erc721.sol"; contract ZombieOwnership is ZombieAttack, ERC721 { mapping (uint => address) zombieApprovals; function balanceOf(address _owner) public view returns (uint256 _balance) { return ownerZombieCount[_owner]; } function ownerOf(uint256 _tokenId) public view returns (address _owner) { return zombieToOwner[_tokenId]; } function _transfer(address _from, address _to, uint256 _tokenId) private { ownerZombieCount[_to]++; ownerZombieCount[_from]--; zombieToOwner[_tokenId] = _to; Transfer(_from, _to, _tokenId); } function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) { _transfer(msg.sender, _to, _tokenId); } function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) { zombieApprovals[_tokenId] = _to; Approval(msg.sender, _to, _tokenId); } function takeOwnership(uint256 _tokenId) public { // 从这里开始 require(zombieApprovals[_tokenId] == msg.sender); address owner = ownerOf(_tokenId); _transfer(owner, msg.sender, _tokenId); } } ## Publication Information - [paedy](https://paragraph.com/@paedy/): Publication homepage - [All Posts](https://paragraph.com/@paedy/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@paedy): Subscribe to updates