【CryptoZombies - 2 Solidity 进阶】009 代币详解与ERC-721实战1(转移)

Malak ·
更新时间:2024-11-13
· 736 次阅读

目录

一、前言

二、代币

1、普通代币

2、以太坊中的代币

3、代币为什么重要

4、ERC-20

5、ERC-721

6、实战1

1.要求

2.代码

7、实战2- balanceOf 和 ownerOf

1.要求

2.代码

三、重构

1、讲解

2、实战1

1.要求

2.代码

四、ERC-721: 转移标准

1、讲解

2、实战1

1.要求

2.代码

3、实战2-transfer

1.要求

2.代码

一、前言

看了一些区块链的教程,论文,在网上刚刚找到了一个项目实战,CryptoZombies。

从这节课开始,我们就开启的新的篇章,这节课是原项目实战的第五课,接下来,让我们一起来学习新的知识吧。

如果你想了解更多有关于机器学习、深度学习、区块链、计算机视觉等相关技术的内容,想与更多大佬一起沟通,那就扫描下方二维码加入我们吧!

二、代币 1、普通代币

如果大家对区块链或者以太坊有比较多的了解,你一定听说过代币,如果你研究过相关的论文,你一定见到过代币。那什么是代币,代币都能用来干嘛呢?

如果不考虑区块链,单纯的来考虑代币,我想,大家能够有个比较直观的印象,如果你能看到我的这篇博客,那你一定能够听说过或者见到过某种类型的代币,不管是在电视上,还是书籍里,还是生活中。

如果你看过跟跟赌有关的电影,有些电影里面玩家双方用一些道具代替纸币,这些道具就是筹码,如果你听说过抓娃娃机,或者玩过抓娃娃机,你能知道,很多时候,我们需要先买一些圆圆的像硬币一样的东西。如果你坐过地铁,有时候,会给你一个地铁卡,有的是像银行卡一样的,有的是像钱币一样圆圆的。

如上这些,都在一定程度上算是代币,所以我们也能基本理解代币,就是说能够代替钱币让我们进行一些操作的东西,我们可以通过这些代换取物品,享受服务,但是只能用于固定用途,比如,抓娃娃机的圆片不能用于地铁。所以他的适用范围是受限制的。

所以,代币的定义如下:

代币,是一种形状及尺寸类似货币,但限制使用范围、不具通货效力的物品。代币通常需要以金钱换取,用在商店、游乐场、大众运输工具等地方,做为凭证以使用服务、换取物品等。代币的材质以金属或塑胶为主。

2、以太坊中的代币

上面说的代币是普通的代币,那以太坊中的代币是什么呢?

以太坊中的代币是加密货币的一种,英文是token,也属于上面提到的代币的范畴,这个代币只能在以太坊平台中使用,用来在以太坊世界中进行各种交易。

以太坊中的代币主要有如下几种:

1.应用代币:保证区块链以太坊的运作,用于激励矿工,创建区块。例如BTC,ETC。应用代币一般采用POW(工作量证明)机制。

2.权益代币:类似公司股权,这是以太坊中新应用的一类代币,权益代币会让其持有者进行收益的分红。区块链黄金交易平台 Digix 通过发行 DGD 的权益代币进行应用众筹。权益代币一般使用POS(权益证明)机制。

3.债权代币:用来解决区块链应用流动性不足的问题。当某个应用突然拥入大量新用户而没有代币来使用应用。为避免其应用代币的价格剧烈波动的原因,又禁止老用户大量抛售自己手中的应用代币。这个时候,这个应用就需要临时借一笔钱,来购买应用代币,以满足新用户的使用需求。这就是债权代币。它类似于为应用提供一个短期的贷款。而对于债权代币的持有者则类似一种储蓄行为,因为一般都可以获得一定的利息回报。

但是我们都知道,以太坊中,都是各种各样的合约,所以从形式上来看:

代币在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了所有其他代币合约共享的一组标准函数。

举个例子:

transfer(address _to, uint256 _value) 
balanceOf(address _owner).

在智能合约内部,通常用下面的映射来追踪每个地址有多少余额:

mapping(address => uint256) balances

 

3、代币为什么重要

我们以上面提到的ERC-20为例,由于所有 ERC-20 代币共享具有相同名称的同一组函数,所以它们都可以以相同的方式进行交互。也就是说我们构建的应用程序能够与一个 ERC-20 代币进行交互,也代表他能与任何 ERC-20 代币进行交互。 这样我们就可以比较轻松地添加更多的代币,我们只需插入新代币的地址,我们的应用就可以使用了。

举个关于交易所的例子,当交易所添加一个新的 ERC20 代币时,实际上它只需要添加与之对话的另一个智能合约。 用户可以让那个合约将代币发送到交易所的钱包地址,然后交易所可以让合约在用户要求取款时将代币发送回给他们。交易所只需要实现这种转移逻辑一次,然后当它想要添加一个新的 ERC20 代币时,只需将新的合约地址添加到它的数据库即可。

4、ERC-20

ERC-20 是一个 以太坊区块链上的智能和约的一种协议标准。其中ERC是Ethereum Request for Comments的缩写。

ERC-20是可替代型代币(同质代币)的标准 API,相当于最基本的代币,包括转账和余额跟踪的功能。

除了ERC-20,还有很多代币标准,比如:ERC-223,ERC-721,ERC-777,ERC-809等等。后续我会专门写一篇文章来讲述代币,也会详细讲解所有的代币标准。

5、ERC-721

ERC-20 是以太坊中最常用的代币标准,对于货币来说,交易过程是可分的,比如我们可以转移0.5以太。

ERC-721和ERC-20很大的一个区别就在于,ERC-721是不可互换,不能分割的,我们只能以整个单位进行交易,并且每个单位都有唯一的ID。

我们的僵尸是独立不可分割的,比如,我们不能生成0.5个僵尸,所以在我们这里,我们要使用的是ERC-721代币。

在使用之前,我们要先看一下ERC-721代币长什么样:

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 库中的当前版本,但在未来正式发布之前它可能会有更改。 所以把这 一个 可能的实现当作考虑,但不要把它作为 ERC-721 代币的官方标准。

 当我们要再添加一个继承,我们之前已经继承过了,那我们怎么实现多个继承呢?我们看如下的示例:

contract SatoshiNakamoto is NickSzabo, HalFinney {
}
6、实战1
1.要求

1.在文件顶部声明我们pragma的版本。

2.将 zombieattack.sol 和 erc721.sol import 进来。

3.声明一个继承 ZombieAttack 和 ERC721 的新合约, 命名为ZombieOwnership。合约的其他部分先留空。

2.代码
pragma solidity >=0.5.0 <0.6.0;
import "./zombieattack.sol";
// Import file here
import "./erc721.sol";
// Declare ERC721 inheritance here
contract ZombieOwnership is ZombieAttack, ERC721 {
}

7、实战2- balanceOf 和 ownerOf

在实战之前,我们先看一下这两个函数的定义方式:

function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);

对于上面的函数,我们只需要一个传入 address 参数,然后返回这个 address 拥有多少代币。对于下面的函数,我们需要传入一个代币 ID 作为参数 (我们的情况就是一个僵尸 ID),然后返回该代币拥有者的 address

1.要求

1.实现 balanceOf 来返回 _owner 拥有的僵尸数量。

2.实现 ownerOf 来返回拥有 ID 为 _tokenId 僵尸的所有者的地址。

2.代码
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) {
    // 1. Return the number of zombies `_owner` has here
    return ownerZombieCount[_owner];
  }
  function ownerOf(uint256 _tokenId) external view returns (address) {
    // 2. Return the owner of `_tokenId` here
    return zombieToOwner[_tokenId];
  }
  function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
  }
  function approve(address _approved, uint256 _tokenId) external payable {
  }
}

三、重构
1、讲解

上面的代码是有些问题的,如果我们编译,会提示我们:

你不能有相同名称的修饰符和函数。

这是因为:

我们定义了一个叫 ownerOf 的函数。之前我们在zombiefeeding.sol 里以 ownerOf 命名创建了一个modifier修饰符。

但是我们不能将这个函数改为别的名称,为什么呢?

因为在ERC-721中,是这样定义的,我们要使用这个标准,就要按照这种方式进行函数定义,所以,我们只能修改我们的修饰符。

 

2、实战1 1.要求

回到了 zombiefeeding.sol, 我们将把 modifier 的名称从 ownerOf 改成 onlyOwnerOf

1.把修饰符定义中的名称改成 onlyOwnerOf

2.往下滑到使用此修饰符的函数 feedAndMultiply 。我们也需要改这里的名称。

2.代码
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;
  // 1. Change modifier name to `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. Change modifier name here as well
  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");
  }
}

四、ERC-721: 转移标准
1、讲解

我们可能需要把我们的东西给别人,比如进行交易,这个时候,我们就要转移我们的所有权。在ERC-721中,有两种不同的方法来实现转移代币:

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 都将包含相同的转移逻辑,只是以相反的顺序。:

第一种方法:代币的发送者调用函数

第二种方法:代币的接收者调用函数

2、实战1 1.要求

我们把这个逻辑抽象成它自己的私有函数 _transfer,然后由这两个函数来调用它。 这样我们就不用写重复的代码了。

1.定义一个名为 _transfer的函数。它会需要3个参数:address _fromaddress _touint256 _tokenId。它应该是一个私有函数。

2.有2个映射会在所有权改变的时候改变: 

(1)ownerZombieCount:记录一个所有者有多少只僵

(2)zombieToOwner :记录什么人拥有什么

改变方式如下:

(1)为接收僵尸的人(address _to)增加ownerZombieCount。使用 ++ 来增加。

(2)为发送僵尸的人(address _from)减少ownerZombieCount。使用 -- 来扣减。

(3)改变 _tokenId 的 zombieToOwner 映射指向 _to

3.ERC-721规范包含了一个 Transfer 事件。这个函数的最后一行应该用正确的参数触发Transfer ——查看 erc721.sol 看它期望传入的参数并在这里实现。

2.代码
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 {
  }
}

3、实战2-transfer
1.要求

1.我们想确保只有代币或僵尸的所有者可以转移它。定义一个名为的映射zombieApprovals。它应将映射uintaddress。这样,当不是所有者的某人使用_tokenId呼叫transferFrom时,我们可以使用此映射快速查找他是否被许可使用该代币。

2.为transferFrom添加一个require声明。该声明应该确保只有拥有者owner 或 核验后的代币/僵尸的地址才能转移它。

其方法如下:

(1)zombieToOwner for _tokenId is equal to msg.sender

(2)zombieApprovals for _tokenId is equal to msg.sender

 

2.代码
pragma solidity >=0.5.0  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 {
    // 2. Add the require statement here
    require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
    // 3. Call _transfer
    _transfer(_from, _to, _tokenId);
  }
  function approve(address _approved, uint256 _tokenId) external payable {
  }
}


作者:水亦心



solidity 实战

需要 登录 后方可回复, 如果你还没有账号请 注册新账号