从这节开始,咱们将学习代币
,ERC721
标准, 以及加密收集资产
等知识。
让咱们来聊聊以太坊上的代币
。数据库
若是你对以太坊的世界有一些了解,你极可能听过人们聊到代币——尤为是 ERC20
代币。数据结构
一个 代币
在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了全部其余代币合约共享的一组标准函数,例如 transfer(address _to, uint256 _value)
和 balanceOf(address _owner)
.app
在智能合约内部,一般有一个映射, mapping(address => uint256) balances
,用于追踪每一个地址还有多少余额。函数
因此基本上一个代币只是一个追踪谁拥有多少该代币的合约,和一些可让那些用户将他们的代币转移到其余地址的函数
。学习
因为全部 ERC20 代币共享具备相同名称的同一组函数,它们均可以以相同的方式进行交互。ui
这意味着若是你构建的应用程序可以与一个 ERC20 代币进行交互,那么它就也可以与任何 ERC20 代币进行交互。 这样一来,未来你就能够轻松地将更多的代币添加到你的应用中,而无需进行自定义编码。 你能够简单地插入新的代币合约地址,而后哗啦,你的应用程序有另外一个它可使用的代币了。编码
其中一个例子就是交易所
。 当交易所添加一个新的 ERC20 代币时,实际上它只须要添加与之对话的另外一个智能合约。 用户可让那个合约将代币发送到交易所的钱包地址,而后交易所可让合约在用户要求取款时将代币发送回给他们。加密
交易所只须要实现这种转移逻辑一次,而后当它想要添加一个新的 ERC20 代币时,只需将新的合约地址添加到它的数据库便可。code
对于像货币同样的代币来讲,ERC20 代币很是酷。 可是要在咱们僵尸游戏中表明僵尸就并非特别有用。继承
首先,僵尸不像货币能够分割 —— 我能够发给你 0.237 以太,可是转移给你 0.237 的僵尸听起来就有些搞笑。
其次,并非全部僵尸都是平等的。 你的2级僵尸"Steve"彻底不能等同于我732级的僵尸"H4XF13LD MORRIS"。(你差得远呢,Steve)。
有另外一个代币标准更适合如 CryptoZombies 这样的加密收藏品——它们被称为ERC721
代币.
ERC721代币
是不能互换的,由于每一个代币都被认为是惟一且不可分割的。 你只能以整个单位交易它们,而且每一个单位都有惟一的 ID。 这些特性正好让咱们的僵尸能够用来交易。
请注意,使用像 ERC721 这样的标准的优点就是,咱们没必要在咱们的合约中实现拍卖或托管逻辑,这决定了玩家可以如何交易/出售咱们的僵尸。 若是咱们符合规范,其余人能够为加密可交易的 ERC721 资产搭建一个交易所平台,咱们的 ERC721 僵尸将能够在该平台上使用。 因此使用代币标准相较于使用你本身的交易逻辑有明显的好处。
咱们将在下一章深刻讨论ERC721的实现。 但首先,让咱们为本课设置咱们的文件结构。
咱们将把全部ERC721逻辑存储在一个叫ZombieOwnership
的合约中。
zombieattack.sol import
进来。ZombieAttack
的新合约, 命名为ZombieOwnership
。合约的其余部分先留空。zombieownership.sol
// 从这里开始 pragma solidity ^0.4.19; import "./zombieattack.sol"; contract ZombieOwnership is ZombieAttack { }
让咱们来看一看 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 。
erc721.sol
导入到 zombieownership.sol
ZombieOwnership
继承自 ZombieAttack
和 ERC721
zombieownership.sol
pragma solidity ^0.4.19; import "./zombieattack.sol"; // 在这里引入文件 import "./erc721.sol"; // 在这里声明 ERC721 的继承 contract ZombieOwnership is ZombieAttack, ERC721 { }
如今,咱们来深刻讨论一下 ERC721
的实现。
咱们已经把全部你须要在本课中实现的函数的空壳复制好了。
在本章节,咱们将实现头两个方法: balanceOf
和 ownerOf
。
function balanceOf(address _owner) public view returns (uint256 _balance);
这个函数只须要一个传入 address
参数,而后返回这个 address
拥有多少代币。
在咱们的例子中,咱们的“代币”是僵尸。你还记得在咱们 DApp 的哪里存储了一个主人拥有多少只僵尸吗?
function ownerOf(uint256 _tokenId) public view returns (address _owner);
这个函数须要传入一个代币 ID
做为参数 (咱们的状况就是一个僵尸 ID),而后返回该代币拥有者的 address
。
一样的,由于在咱们的 DApp 里已经有一个 mapping
(映射) 存储了这个信息,因此对咱们来讲这个实现很是直接清晰。咱们能够只用一行 return
语句来实现这个函数。
注意:要记得, uint256 等同于uint。咱们从课程的开始一直在代码中使用 uint,但从如今开始咱们将在这里用 uint256,由于咱们直接从规范中复制粘贴。
我将让你来决定如何实现这两个函数。
每一个函数的代码都应该只有1行 return
语句。看看咱们在以前课程中写的代码,想一想咱们都把这个数据存储在哪。若是你以为有困难,你能够点“我要看答案”的按钮来得到帮助。
balanceOf
来返回 _owner
拥有的僵尸数量。ownerOf
来返回拥有 ID 为 _tokenId
僵尸的全部者的地址。zombieownership.sol
pragma 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
。
onlyOwnerOf
feedAndMultiply
。咱们也须要改这里的名称。注意:咱们在 zombiehelper.sol 和 zombieattack.sol 里也使用了这个修饰符,因此这两个文件也必须把名字改了。
zombiefeeding.sol
pragma 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 规范有两种不一样的方法来转移代币:
function transfer(address _to, uint256 _tokenId) public; function approve(address _to, uint256 _tokenId) public; function takeOwnership(uint256 _tokenId) public;
transfer
方法,传入他想转移到的 address
和他想转移的代币的 _tokenId
。approve
,而后传入与以上相同的参数。接着,该合约会存储谁被容许提取代币,一般存储到一个 mapping (uint256 => address)
里。而后,当有人调用 takeOwnership
时,合约会检查 msg.sender
是否获得拥有者的批准来提取代币,若是是,则将代币转移给他。你注意到了吗,transfer
和takeOwnership
都将包含相同的转移逻辑,只是以相反的顺序。 (一种状况是代币的发送者调用函数;另外一种状况是代币的接收者调用它)。
因此咱们把这个逻辑抽象成它本身的私有函数 _transfer
,而后由这两个函数来调用它。 这样咱们就不用写重复的代码了。
让咱们来定义 _transfer
的逻辑。
_transfer
的函数。它会须要3个参数:address _from
、address _to
和uint256 _tokenId
。它应该是一个 私有
函数。ownerZombieCount
(记录一个全部者有多少只僵尸)和 zombieToOwner
(记录什么人拥有什么)。address _to
)增 加ownerZombieCount
。使用 ++
来增长。address _from
)减小ownerZombieCount
。使用 -- 来扣减。_tokenId
的 zombieToOwner
映射,这样它如今就会指向 _to
。ERC721规范包含了一个 Transfer 事件。这个函数的最后一行应该用正确的参数触发Transfer ——查看 erc721.sol 看它指望传入的参数并在这里实现。
zombieownership.zol
pragma 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
函数几乎已经把全部的重活都干完了。
onlyOwnerOf
添加到这个函数中。msg.sender
做为参数传递进 address _from
。zombieownership.zol
pragma 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 { } }
如今,让咱们来实现 approve
。
记住,使用 approve
或者 takeOwnership
的时候,转移有2个步骤:
address
和你但愿他获取的 _tokenId
来调用 approve
_tokenId
来调用 takeOwnership
,合约会检查确保他得到了批准,而后把代币转移给他。由于这发生在2个函数的调用中,因此在函数调用之间,咱们须要一个数据结构来存储什么人被批准获取什么。
zombieApprovals
。它应该将一个 uint
映射到一个 address
。_tokenId
的 zombieApprovals
设置为和 _to
相等。Approval
事件。因此咱们应该在这个函数的最后触发这个事件。(参考 erc721.sol 来确认传入的参数,并确保 _owner 是 msg.sender)zombieownership.zol
pragma 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
, 应该只是简单地检查以确保 msg.sender
已经被批准来提取这个代币或者僵尸。若确认,就调用 _transfer
;
require
句式来检查 _tokenId
的 zombieApprovals
和 msg.sender
相等。msg.sender
未被受权来提取这个代币,将抛出一个错误。注意: 咱们彻底能够用一行代码来实现第二、3两步。可是分开写会让代码更易读。一点我的建议 :)
zombieownership.zol
pragma 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); } }