ERC721和加密收藏品
唷!这里的气氛开始升温了……
在这节课中,我们将更深入一些。
我们将讨论代币、ERC721标准和加密可收集资产。
换句话说,我们要做的是让你可以和朋友交换僵尸。
一、以太坊上的代币
我们来谈谈代币。
如果你在以太坊领域呆过一段时间,你可能听过人们谈论代币——特别是ERC20代币
。
以太坊上的token
基本上只是一个遵循一些通用规则的智能合约,即它实现了所有其他令牌合约共享的一组标准函数,例如transferFrom(addres_from, addres_to, uint256 _amount)
和balanceOf(addres_owner)
。
在内部,智能合约通常有一个映射,映射(address=> uint256
)余额,跟踪每个地址有多少余额。
基本上,代币只是一个合约,它记录了谁拥有多少代币,以及一些功能,以便这些用户可以将他们的代币转移到其他地址
它为什么重要
由于所有ERC20令牌共享具有相同名称的相同函数集,因此它们都可以以相同的方式进行交互。
这意味着如果您构建的应用程序能够与一个ERC20令牌进行交互,那么它也能够与任何ERC20令牌进行交互。这样就可以在未来轻松地将更多令牌添加到应用程序中,而无需自定义编码。你可以简单地插入新的令牌合约地址,然后,你的应用程序就有了另一个可以使用的令牌。
其中一个例子就是交易所。当交易所添加新的ERC20令牌时,实际上它只需要添加另一个与之对话的智能合约。用户可以告诉合约将代币发送到交易所的钱包地址,而交易所可以告诉合约在用户请求提现时将代币发送回给用户。
交易所只需要实现此传输逻辑一次,然后当它想要添加新的ERC20令牌时,只需将新的合约地址添加到其数据库中即可。
其他token标准
ERC20代币对于像货币一样的代币来说真的很酷。但在我们的僵尸游戏中,它们并不能代表僵尸。
首先,僵尸不像货币那样可分割——我可以给你0.237个ETH,但给你0.237个僵尸真的没有意义。
其次,并非所有僵尸都生来平等。你的2级僵尸“Steve”完全不等于我的732级僵尸“H4XF13LD MORRIS💯💯😎💯💯”。(差远了,史蒂夫)。
还有另一种代币标准,更适合像CryptoZombies这样的加密收藏品,它们被称为ERC721 token
。
ERC721 token
是不可互换的,因为每个令牌都被认为是唯一的,并且不可分割。你只能以整个单位交易它们,每个单位都有一个唯一的ID。所以这些非常适合让我们的僵尸可以交易。
注意,使用像ERC721这样的标准有一个好处,那就是我们不必在合同中执行拍卖或托管逻辑,这些逻辑决定了玩家如何交易/出售我们的僵尸。如果我们符合规范,其他人可以为可加密交易的ERC721资产构建交换平台,而我们的ERC721僵尸将在该平台上可用。因此,使用代币标准而不是滚动自己的交易逻辑有明显的好处
实战演习
我们将在下一章深入研究ERC721的实现。但首先,让我们为本课设置文件结构。
我们将把所有ERC721逻辑存储在一个名为ZombieOwnership的合约中。
译文:
- 在文件的顶部声明我们的pragma版本(查看前面课程的文件以了解语法)。
- 这个文件应该从zombieattack.sol中导入。
- 声明一个新的合约,ZombieOwnership,继承自ZombieAttack。暂时把合同的主体部分空着。
合约修改
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
contract ZombieOwnership is ZombieAttack {
}
ERC721标准,多重继承
我们来看一下ERC721标准:
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
}
这是我们需要实现的方法列表,我们将在接下来的章节中逐个实现。
看起来很多,但不要不知所措!我们来帮你解决。
实现代币合约
在实现代币合约时,我们要做的第一件事是将接口复制到它自己的Solidity文件中并导入,import “./erc721.sol”;
然后我们从它继承我们的契约,我们用函数定义覆盖每个方法。
但是等等-ZombieOwnership
已经继承了ZombieAttack
它怎么能继承ERC721
?
幸运的是,在Solidity中,你的合约可以继承多个合约,如下所示
contract SatoshiNakamoto is NickSzabo, HalFinney {
// Omg, the secrets of the universe revealed!
}
正如你所看到的,当使用多重继承时,你只需要用逗号,,来分隔你要继承的多个契约。在这种情况下,我们的合同继承自NickSzabo
and HalFinney
.
让我们试一试。
实战演习
我们已经用上面的接口创建了erc721.sol
。
- 在
ombieownership.sol
里导入erc721.sol
- 声明
ZombieOwnership
继承自ZombieAttack
和ERC721
合约修改
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
import "./erc721.sol";
// Import file here
// Declare ERC721 inheritance here
contract ZombieOwnership is ZombieAttack, ERC721 {
}
三、balanceOf & ownerOf
很好,让我们深入了解ERC721的实现!
我们已经复制了本课中将要实现的所有函数的空shell。
在本章中,我们将实现前两个方法:balanceOf
和ownerOf
balanceOf
function balanceOf(address _owner) external view returns (uint256 _balance);
该函数只接受一个address
,并返回该地址拥有的令牌数量。
在我们的例子中,我们的“代币”是僵尸。你还记得我们在DApp中存储了多少僵尸吗?
ownerOf
function ownerOf(uint256 _tokenId) external view returns (address _owner);
这个函数接受一个 token ID(在我们的例子中是一个Zombie ID),并返回拥有它的人的address
。
同样,这对我们来说实现起来非常简单,因为我们在DApp中已经有了一个存储这些信息的映射。我们可以在一行中实现这个函数,只需要一个return语句。
注意:记住,uint256等价于int。到目前为止,我们一直在代码中使用int,但我们在这里使用uint256,因为我们从规范中复制/粘贴。
实战演习
我将把如何实现这两个函数的问题留给您自己去解决。
每个函数应该只有一行代码,一个返回语句。看一下前几课的代码,看看我们在哪里存储这些数据。如果你想不出来,你可以点击“show me the answer”按钮寻求帮助。
-
实现
balanceOf
返回_owner
拥有的僵尸数。 -
实现
balanceOf
返回_owner
拥有的僵尸数。
合约修改
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
function balanceOf(address _owner) external view returns (uint256) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) external view returns (address) {
return zombieToOwner[_tokenId];
}
function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
}
function approve(address _approved, uint256 _tokenId) external payable {
}
}
四、重构
哦哦!我们刚刚在代码中引入了一个错误,使它无法编译。你注意到了吗?
在前一章中,我们定义了一个名为ownerOf
的函数。但是如果您还记得第4课,我们还在zombiefeeder .so
l中创建了一个同名的修饰符ownerOf
。
如果你尝试编译这段代码,编译器会给你一个错误,说你不能有一个修饰符和一个同名的函数。
那么我们是否应该将ZombieOwnership
中的函数名更改为其他内容?
不,我们不能那样做!!请记住,我们使用的是ERC721 token标准,这意味着其他合约将期望我们的合约具有定义了这些确切名称的函数。这就是这些标准有用的地方——如果另一个合约知道我们的合约是ERC721兼容的,它可以简单地与我们对话,而不需要知道我们内部实现决策的任何信息。
因此,这意味着我们必须重构第4课中的代码,将modifier
的名称更改为其他名称
实战演习
回到zombiefeeding.sol
我们要把修饰符的名字从ownerOf
改成onlyOwnerOf
。
- 将修饰符定义的名称更改为
onlyOwnerOf
向下滚动到feedAndMultiply
函数,它使用了这个modifier
。我们还需要把这里的名字也改一下。
注意:我们也在
zombiehelper.sol
和zombieattack.sol
中使用这个modifier
。但我们不会花太多时间在重构上,我们已经为你改变了这些文件中的修饰符名。
合约修改
pragma solidity >=0.5.0 <0.6.0;
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;
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);
}
function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) internal onlyOwnerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
require(_isReady(myZombie));
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("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: Transfer Logic
太好了,我们解决了冲突!
现在,我们将通过将所有权从一个人转移到另一个人来继续我们的ERC721实现。
请注意,ERC721规范有两种不同的方式来传输tokens
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
和
function approve(address _approved, uint256 _tokenId) external payable;
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
- 第一种方法是令牌的所有者调用
transferFrom
,使用他的地址作为_from
参数,他想要传输的地址作为_to
参数,以及他想要传输的令牌的_tokenId
。 - 第二种方法是令牌的所有者首先使用他想要转移到的地址和
_tokenID
调用approve。然后合约存储谁被批准接受令牌,通常在mapping (uint256 => address).
中。然后,当所有者或批准的地址调用transferFrom
时,合约检查msg.sender
是否是所有者,或者被所有者批准接受令牌,如果是这样,它将令牌转移给他。
注意,这两个方法包含相同的传输逻辑。在一种情况下,令牌的发送方调用transferFrom
函数;在另一种情况下,令牌的所有者或批准的接收者调用它。
因此,将这个逻辑抽象到它自己的私有函数_transfer
中是有意义的,然后由transferFrom
调用该函数。
实战演习
让我们定义_transfer的逻辑。
- 定义一个名为
_transfer
的函数。它将接受3个参数,addres_from
,addres_to
和uint256 _tokenId
。这应该是private
函数。 - 当所有权发生变化时,我们有2个映射会发生变化:
ownerZombieCount
(跟踪所有者拥有多少僵尸)和zombieToOwner
(跟踪谁拥有什么)。
我们的函数应该做的第一件事是为接收僵尸的人增加ownerZombieCount
(addres_to
)。用++
递增。 - 接下来,我们需要减少发送僵尸的人(
addres_from
)的ownerZombieCount
。使用--
来递减。 - 最后,我们想要改变这个
_tokenId
的zombieToOwner
映射,所以它现在指向_to
。 - 我们还有一件事要做。
ERC721规范包括一个Transfer
事件。这个函数的最后一行应该用正确的信息触发Transfer
检查erc721。看看它期望用什么参数被调用并在这里实现它。
合约修改
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
function balanceOf(address _owner) external view returns (uint256) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) external view returns (address) {
return zombieToOwner[_tokenId];
}
// Define _transfer() here
function _transfer(address _from, address _to, uint256 _tokenId) private{
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
emit Transfer(_from, _to, _tokenId);
}
function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
}
function approve(address _approved, uint256 _tokenId) external payable {
}
}
六、ERC721:转移控制
太棒了!这是比较困难的部分——现在实现外部transferFrom
函数会很容易,因为_transfer
函数已经完成了几乎所有繁重的工作。
实战演习
- 首先,我们要确保只有token/僵尸的所有者或批准的地址才能传输它。让我们定义一个名为
zombieApprovals
的映射。它应该把一个int
映射到一个address
。这样,当非所有者使用_tokenId
调用transferFrom
时,我们可以使用此映射快速查找他是否被批准使用该token。 - 接下来,让我们给transferFrom添加一个
require
语句。它应该确保只有令牌/僵尸的所有者或批准的地址才能传输它。 - 最后,不要忘记调用
_transfer
。
注意:检查只有token/僵尸的所有者或批准的地址才能转移它,意味着必须至少满足以下条件之一:
ombieToOwner
for_tokenId
is equal tomsg.sender
orzombieApprovals
for_tokenId
is equal tomsg.sender
在zombieApprovals
映射中填充数据,我们将在下一章中完成。
合约修改
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
mapping (uint => address) zombieApprovals;
function balanceOf(address _owner) external view returns (uint256) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) external view returns (address) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
emit Transfer(_from, _to, _tokenId);
}
function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
_transfer(_from, _to, _tokenId);
}
function approve(address _approved, uint256 _tokenId) external payable {
}
}
七、 ERC721: Approve
现在,让我们实现approve
。
记住,使用approve
后,转账分两个步骤进行:
- 所有者,调用
approve
并给它新所有者的_approved
地址,以及你想让他们接受的_tokenId
。 - 新的所有者使用
_tokenId
调用transferFrom
。接下来,合约检查以确保新所有者已经获得批准,然后将令牌转移给他们。
因为这发生在两个函数调用中,所以我们需要使用zombieApprovals
数据结构来存储在函数调用之间谁被批准了什么。
实战演习
- 在
approve
函数中,我们希望确保只有令牌的所有者可以批准某人使用它。所以我们需要添加onlyOwnerOf
修饰符来批准 - 对于函数体,设置
_tokenId
的zombieApprovals
等于_approved
的地址。
合约修改
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
mapping (uint => address) zombieApprovals;
function balanceOf(address _owner) external view returns (uint256) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) external view returns (address) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
emit Transfer(_from, _to, _tokenId);
}
function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
_transfer(_from, _to, _tokenId);
}
function approve(address _approved, uint256 _tokenId) external payable onlyOwnerOf(_tokenId) {
zombieApprovals[_tokenId] = _approved;
}
}
八、ERC721: Approve
太好了,我们差不多完成了!
还有一件事要做——ERC721规范中有一个Approval
事件。所以我们应该在approve
函数的末尾触发这个事件。
实战演习
pragma solidity >=0.5.0 <0.6.0;
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
}
让我们启动 Approval
事件。看看erc721.sol文件中的参数,并确保使用msg.sender
作为_owner
合约修改
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
mapping (uint => address) zombieApprovals;
function balanceOf(address _owner) external view returns (uint256) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) external view returns (address) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
emit Transfer(_from, _to, _tokenId);
}
function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
_transfer(_from, _to, _tokenId);
}
function approve(address _approved, uint256 _tokenId) external payable onlyOwnerOf(_tokenId) {
zombieApprovals[_tokenId] = _approved;
emit Approval(msg.sender, _approved, _tokenId);
}
}
九、 防止溢出
祝贺你,完成了ERC721和ERC721x的实现!
也没那么难吧?当你听到人们谈论它的时候,很多以太坊的东西听起来真的很复杂,所以理解它的最好方法是自己实际实现它。
请记住,这只是一个最小的实现。我们可能想在实现中添加一些额外的功能,比如一些额外的检查,以确保用户不会意外地将他们的僵尸转移到地址0
(这被称为“燃烧”一个token——基本上它被发送到一个没有人拥有私钥的地址,基本上使它无法恢复)。或者在DApp中放入一些基本的拍卖逻辑。(你能想到一些我们可以实现的方法吗?)
但是我们想让这个课程易于管理,所以我们使用了最基本的实现。如果您想查看更深入实现的示例,可以在本教程结束后查看OpenZeppelin ERC721契约。
合约安全性增强:溢出和下溢
我们将研究在编写智能合约时应该注意的一个主要安全特性:防止溢出和下溢。
什么是溢出
?
假设我们有一个uint8
类型,它只能有8位。这意味着我们可以存储的最大数字是二进制11111111
(或者十进制,2^8 - 1 = 255)。
看看下面的代码。最后number
等于多少?
uint8 number = 255;
number++;
在这种情况下,我们导致了它溢出,所以number
现在等于0
,尽管我们增加了它。(如果在二进制11111111
上加1
,它会复位为00000000
,就像时钟从23:59转到00:00一样)。
下溢出是类似的,如果你从等于0
的uint8
中减去1
,它现在等于255
(因为单位是无符号的,不能是负数)。
虽然我们在这里不使用uint8
,而且每次增加1
(2^256是一个非常大的数字)时,uint256
似乎不太可能溢出,但在我们的合约中放入保护措施仍然是很好的,这样我们的DApp将来就不会有意外的行为。
使用SafeMath
为了防止这种情况,OpenZeppelin创建了一个名为SafeMath的库,默认情况下可以防止这些问题。
但在我们讨论这个之前…library是什么?
library是solid中的一种特殊类型的合约。它的用处之一是将函数附加到本机数据类型。
例如,对于SafeMath库,我们将使用使用SafeMath为uint256的
语法。SafeMath库有4个函数- add, sub, mul和div
。现在我们可以通过如下函数使用uint256
:
使用SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
我们将在下一章中看到这些函数的作用,但是现在让我们将SafeMath库添加到我们的合约中。
实战演习
我们已经在SafeMath .sol中为您包含了OpenZeppelin的SafeMath库。如果您愿意,现在可以快速浏览一下代码,但我们将在下一章中深入研究它。
首先让我们告诉我们的合约使用SafeMath。我们将在ZombieFactory中这样做,这是我们的基本合约——这样我们就可以在继承这个合约的任何子合约中使用它。
- 在
zombiefactory.sol
导入safemath.sol
. - 用
SafeMath
为uint256
添加声明;.
合约修改
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
import "./erc721.sol";
import "./safemath.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
using SafeMath for uint256;
mapping (uint => address) zombieApprovals;
function balanceOf(address _owner) external view returns (uint256) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) external view returns (address) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
emit Transfer(_from, _to, _tokenId);
}
function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
_transfer(_from, _to, _tokenId);
}
function approve(address _approved, uint256 _tokenId) external payable onlyOwnerOf(_tokenId) {
zombieApprovals[_tokenId] = _approved;
emit Approval(msg.sender, _approved, _tokenId);
}
}
十、SafeMath Part 2
让我们看一下SafeMath背后的代码:
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
首先是library关键字——库类似于合约,但有一些不同之处。出于我们的目的,标准库允许使用using关键字,它自动将标准库的所有方法附加到另一个数据类型:
using SafeMath for uint;
// now we can use these methods on any uint
uint test = 2;
test = test.mul(3); // test now equals 6
test = test.add(5); // test now equals 11
注意,mul和add函数都需要2个参数,但是当我们使用SafeMath for uint声明时,我们调用函数的uint (test)会自动作为第一个参数传递进来。
让我们来看看add背后的代码,看看SafeMath是做什么的:
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
基本上add只是将2个单位相加,但它也包含一个assert语句,以确保总和大于a。这可以保护我们免受溢出。
Assert类似于require,如果为false则抛出错误。assert和require之间的区别在于,当函数失败时,require会退还用户剩余的gas,而assert则不会。所以大多数时候你想在代码中使用require;Assert通常在代码出现严重错误(如int溢出)时使用。
因此,简单地说,SafeMath的add、sub、mul和div
都是执行基本数学操作的函数,但是如果发生溢出或下溢则 抛出错误
在代码中使用SafeMath
为了防止溢出和下溢,我们可以在代码中查找使用+、-、*或/
的位置,并用add、sub、mul、div
替换它们。
例:
myUint++;
替代为
myUint = myUint.add(1);
实战演习
我们在 ZombieOwnership
中有2个地方使用了数学运算。让我们用SafeMath方法替换它们。
-
Replace
++
with a SafeMath method. -
Replace
--
with a SafeMath method.
合约修改
unction _transfer(address _from, address _to, uint256 _tokenId) private {
// 1. Replace with SafeMath's `add`
ownerZombieCount[_to] = ownerZombieCount[_to].add(1);
//wnerZombieCount[_to]++;
// 2. Replace with SafeMath's `sub`
//ownerZombieCount[_from]--;
ownerZombieCount[_from] = ownerZombieCount[_from].sub(1);
zombieToOwner[_tokenId] = _to;
emit Transfer(_from, _to, _tokenId);
}
十一、SafeMath Part 3
很好,现在我们的ERC721实现可以避免溢出和下溢了!
回顾我们在前几课中编写的代码,我们的代码中还有其他一些地方可能容易出现溢出或下溢。
例如,在ZombieAttack
中,我们拥有:
myZombie.winCount++;
myZombie.level++;
enemyZombie.lossCount++;
为了安全起见,我们也应该防止这里的溢出。(一般来说,只使用SafeMath而不使用基本的数学运算是个好主意。也许在未来的Solidity版本中,这些功能会默认实现,但现在我们必须在代码中采取额外的安全预防措施)。
然而,我们有一个小问题 winCount
和lossCount
是uint16
,level
是uint32。因此,如果我们使用SafeMath的add
方法将这些作为参数,它实际上不会保护我们免受溢出,因为它会将这些类型转换为uint256
:
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
// If we call `.add` on a `uint8`, it gets converted to a `uint256`.
// So then it won't overflow at 2^8, since 256 is a valid `uint256`.
这意味着我们需要实现2个库来防止uint16
和uint32
类型的溢出/下溢。我们可以叫它们SafeMath16和SafeMath32。
代码将与SafeMath完全相同,除了uint256的所有实例将被替换为uint32或uint16。
我们已经为你实现了这些代码-继续看看safemath。请查看代码。
现在我们需要在ZombieFactory中执行它。
实战演习
- 声明我们对
uint32
使用SafeMath32。 - 声明我们使用SafeMath16作为
uint16
类型。 - 在
ZombieFactory
中还有一行代码需要使用SafeMath方法。我们已经留下了评论来指出在哪里
合约修改
pragma solidity >=0.5.0 <0.6.0;
import "./ownable.sol";
import "./safemath.sol";
contract ZombieFactory is Ownable {
using SafeMath for uint256;
using SafeMath32 for uint32;
using SafeMath16 for uint16;
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
uint cooldownTime = 1 days;
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
uint16 winCount;
uint16 lossCount;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string memory _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].add(1);
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string memory _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string memory _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
}
十二、SafeMath Part 4
很好,现在我们可以在DApp中使用的所有类型的单位上实现SafeMath了!
让我们修复《僵尸攻击》中的所有潜在问题。(还有一个zombies[_zombieId].level++;
这需要在 ZombieHelper
中修复,但我们已经为您解决了这个问题,所以我们不需要额外的章节来做😉)。
实战演习
继续在ZombieAttack中的所有增量上执行SafeMath方法。我们在代码中留下了注释,以便于查找。
合约修改
pragma solidity >=0.5.0 <0.6.0;
import "./zombiehelper.sol";
contract ZombieAttack is ZombieHelper {
uint randNonce = 0;
uint attackVictoryProbability = 70;
function randMod(uint _modulus) internal returns(uint) {
// Here's one!
randNonce = randNonce.add(1);
return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;
}
function attack(uint _zombieId, uint _targetId) external onlyOwnerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
Zombie storage enemyZombie = zombies[_targetId];
uint rand = randMod(100);
if (rand <= attackVictoryProbability) {
// Here's 3 more!
myZombie.winCount = myZombie.winCount.add(1);
myZombie.level = myZombie.level.add(1);
enemyZombie.lossCount = enemyZombie.lossCount.add(1);
feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
} else {
// ...annnnd another 2!
myZombie.lossCount = myZombie.lossCount.add(1);
enemyZombie.winCount = enemyZombie.winCount.add(1);
_triggerCooldown(myZombie);
}
}
}
十三、 Comments
我们僵尸游戏的Solidity代码终于完成了!
在接下来的课程中,我们将了解如何将代码部署到以太坊,以及如何使用Web3.js与之交互。
但是在我们开始第5课之前还有最后一件事:让我们讨论一下注释代码。
注释语法
在Solidity中注释就像JavaScript一样。在CryptoZombies课程中,你已经看到了一些单行注释的例子:
// This is a single-line comment. It's kind of like a note to self (or to others)
只要在任何地方加上双引号//,你就在注释了。这很容易,你应该一直做。
但我明白你的意思,有时候一句话是不够的。毕竟,你天生就是个作家!
因此我们也有多行注释:
contract CryptoZombies {
/* This is a multi-lined comment. I'd like to thank all of you
who have taken your time to try this programming course.
I know it's free to all of you, and it will stay free
forever, but we still put our heart and soul into making
this as good as it can be.
Know that this is still the beginning of Blockchain development.
We've come very far but there are so many ways to make this
community better. If we made a mistake somewhere, you can
help us out and open a pull request here:
https://github.com/loomnetwork/cryptozombie-lessons
Or if you have some ideas, comments, or just want to say
hi - drop by our Telegram community at https://t.me/loomnetworkdev
*/
}
特别是,注释代码以解释合约中每个函数的预期行为是一种很好的做法。这样,其他开发人员(或者你,在项目中断6个月之后!)就可以快速浏览和理解你的代码做了什么,而不必阅读代码本身。
Solidity社区的标准是使用一种叫做natspec的格式,它看起来是这样的
/// @title A contract for basic math operations
/// @author H4XF13LD MORRIS 💯💯😎💯💯
/// @notice For now, this contract just adds a multiply function
contract Math {
/// @notice Multiplies 2 numbers together
/// @param x the first uint.
/// @param y the second uint.
/// @return z the product of (x * y)
/// @dev This function does not currently check for overflows
function multiply(uint x, uint y) returns (uint z) {
// This is just a normal comment, and won't get picked up by natspec
z = x * y;
}
}
@title
和@author
很简单。@notice
向用户解释合约/函数的作用。@dev
用于向开发人员解释额外的细节。@param
和@return
用于描述函数的每个参数和返回值的作用。
注意,您不必总是为每个函数使用所有这些标记—所有标记都是可选的。但至少,要留下@dev
注释,解释每个函数的作用。
实战演习
如果你现在还没有注意到,CryptoZombies答案检查器在检查你的答案时会忽略评论。所以我们实际上不能检查这一章的natspec代码;)
然而,到目前为止,你是一个坚实的能手-我们只是假设你有这个!
尝试一下,并尝试添加一些natspec标签到ZombieOwnership:
-
@title
-例如,管理僵尸所有权转移的合约 -
@author
-你的名字! -
@dev
-例如:符合OpenZeppelin对ERC721规范草案的实现
十四、 结束演讲
恭喜你!第五课结束了。
作为奖励,我们已经转移了你自己的10级H4XF13LD莫里斯💯💯😎💯💯僵尸!
(Omg,传说中的H4XF13LD MORRIS💯💯😎💯💯僵尸!!!111)
现在你的军队里有4个僵尸。
在继续之前,如果您愿意,可以通过单击它们的右侧并输入新名称来重命名它们中的任何一个。(虽然我不知道为什么你会想重命名H4XF13LD MORRIS💯💯😎💯💯,这显然是有史以来最好的名字)。
我们来总结一下:
本节课我们学习了:
-
代币、ERC721标准和可交易资产/僵尸
-
库以及如何使用它们
-
如何使用SafeMath库防止溢出和下溢文章来源:https://www.toymoban.com/news/detail-838545.html
-
注释代码和natspec标准
本课总结了我们游戏的solid代码!(现在-我们可能会在未来增加更多的课程)。
在接下来的2节课中,我们将学习如何使用web3.js部署合约并与之交互(这样你就可以为你的DApp构建一个前端)。
继续并重命名任何你的僵尸,如果你喜欢,然后继续到下一章完成文章来源地址https://www.toymoban.com/news/detail-838545.html
到了这里,关于ERC721和加密收藏品(ERC721 & Crypto-Collectibles)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!