美文网首页智能合约
【CryptoZombies|编写区块链游戏学智能合约】Less

【CryptoZombies|编写区块链游戏学智能合约】Less

作者: 空灵一月 | 来源:发表于2018-05-03 06:34 被阅读98次

    CryptoZombies是个在编游戏的过程中学习Solidity智能合约语言的互动教程。本教程是为了Solidity初学者而设计的,会从最基础开始教起,即便你从来没有接触过Solidity也可以学,CryptoZombies会手把手地教你。


    1.以太坊上的代币

    如果你对以太坊的有一些了解,你一定听过人们聊到代币——尤其是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僵尸将可以在该平台上使用。 所以使用代币标准相较于使用你自己的交易逻辑有明显的好处。

    2.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;
    }
    

    这是我们需要实现的方法列表,我们将在接下来的章节中逐个学习。虽然看起来很多,但不要被吓到了!我们在这里就是准备带着你一步一步了解它们的。

    实现一个代币合约

    在实现一个代币合约的时候,我们首先要做的是将接口复制到它自己的 Solidity文件并导入它,import ./erc721.sol。 接着,让我们的合约继承它,然后我们用一个函数定义来重写每个方法。在Solidity,你的合约可以继承自多个合约,参考如下:

    contract SatoshiNakamoto is NickSzabo, HalFinney {
      // 啧啧啧,宇宙的奥秘泄露了
    }
    

    正如你所见,当使用多重继承的时候,你只需要用逗号 , 来隔开几个你想要继承的合约。在上面的例子中,我们的合约继承自NickSzabo和HalFinney。

    3.balanceOf和ownerOf

    我们来深入讨论一下ERC721的实现。在本节我们先介绍两个方法: balanceOf 和 ownerOf。

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

    这个函数只需要一个传入 address 参数,然后返回这个 address 拥有多少代币。

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

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

    同样的,因为在我们的 DApp 里已经有一个 mapping (映射) 存储了这个信息,所以对我们来说这个实现非常直接清晰。我们可以只用一行 return 语句来实现这个函数。

    4.ERC721: 转移标准

    现在我们将通过学习把所有权从一个人转移给另一个人来继续我们的 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都将包含相同的转移逻辑,只是以相反的顺序。 (一种情况是代币的发送者调用函数;另一种情况是代币的接收者调用它)。

    5.预防溢出

    合约安全增强: 溢出和下溢

    我们将来学习你在编写智能合约的时候需要注意的一个主要的安全特性:防止溢出和下溢。

    什么是溢出 (overflow)?假设我们有一个uint8, 只能存储8 bit数据。这意味着我们能存储的最大数字就是二进制 11111111 (或者说十进制的 2^8 - 1 = 255)。来看看下面的代码。最后 number 将会是什么值?

    uint8 number = 255;
    number++;
    

    在这个例子中,我们导致了溢出 — 虽然我们加了1, 但是 number 出乎意料地等于 0了。 (如果你给二进制 11111111 加1, 它将被重置为 00000000,就像钟表从 23:59 走向 00:00)。

    下溢(underflow)也类似,如果你从一个等于 0 的 uint8 减去 1, 它将变成 255 (因为 uint 是无符号的,其不能等于负数)。

    虽然我们在这里不使用 uint8,而且每次给一个uint256加1也不太可能溢出 (2^256 真的是一个很大的数了),在我们的合约中添加一些保护机制依然是非常有必要的,以防我们的DApp以后出现什么异常情况。

    使用 SafeMath

    为了防止这些情况,OpenZeppelin建立了一个叫做 SafeMath 的库(library),默认情况下可以防止这些问题。

    比如,使用 SafeMath 库的时候,我们将使用using SafeMath for uint256这样的语法。 SafeMath库有四个方法 — add, sub, mul, div。现在我们可以这样来让 uint256 调用这些方法:

    using SafeMath for uint256;
    
    uint256 a = 5;
    uint256 b = a.add(3); // 5 + 3 = 8
    uint256 c = a.mul(2); // 5 * 2 = 10
    

    来看看 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;
      }
    }
    

    库允许我们使用using关键字,它可以自动把库的所有方法添加给一个数据类型:

    using SafeMath for uint;
    // 这下我们可以为任何 uint 调用这些方法了
    uint test = 2;
    test = test.mul(3); // test 等于 6 了
    test = test.add(5); // test 等于 11 了
    

    注意 mul 和 add 其实都需要两个参数。 在我们声明了 using 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 只是像 + 一样对两个 uint 相加, 但是它用一个 assert 语句来确保结果大于 a。这样就防止了溢出。assert 和 require 相似,若结果为否它就会抛出错误。 assert 和 require 区别在于,require 若失败则会返还给用户剩下的 gas, assert 则不会。所以大部分情况下,你写代码的时候会比较喜欢 require,assert 只在代码可能出现严重错误的时候使用,比如 uint 溢出。

    所以简而言之, SafeMath 的 add, sub, mul, 和 div 方法只做简单的四则运算,然后在发生溢出或下溢的时候抛出错误。


    6.注释

    这一节我们来谈谈如何给你的代码添加注释。Solidity 里的注释和 JavaScript 相同。在我们的课程中你已经看到了不少单行注释了:

    // 这是一个单行注释,可以理解为给自己或者别人看的笔记
    

    只要在任何地方添加一个//就意味着你在注释。如此简单所以你应该经常这么做。不过我们也知道你的想法:有时候单行注释是不够的。所以我们有了多行注释:

    contract CryptoZombies { 
      /* 这是一个多行注释。我想对所有花时间来尝试这个编程课程的人说声谢谢。
      它是免费的,并将永远免费。但是我们依然倾注了我们的心血来让它变得更好。
    
       要知道这依然只是区块链开发的开始而已,虽然我们已经走了很远,
       仍然有很多种方式来让我们的社区变得更好。
       如果我们在哪个地方出了错,欢迎在我们的 github 提交 PR 或者 issue 来帮助我们改进:
        https://github.com/loomnetwork/cryptozombie-lessons
    
        或者,如果你有任何的想法、建议甚至仅仅想和我们打声招呼,欢迎来我们的电报群:
         https://t.me/loomnetworkcn
      */
    }
    

    特别是,最好为你合约中每个方法添加注释来解释它的预期行为。这样其他开发者(或者你自己,在6个月以后再回到这个项目中)可以很快地理解你的代码而不需要逐行阅读所有代码。

    Solidity 社区所使用的一个标准是使用一种被称作natspec的格式,看起来像这样:

    /// @title 一个简单的基础运算合约
    /// @author H4XF13LD MORRIS 💯💯😎💯💯
    /// @notice 现在,这个合约只添加一个乘法
    contract Math {
      /// @notice 两个数相乘
      /// @param x 第一个 uint
      /// @param y  第二个 uint
      /// @return z  (x * y) 的结果
      /// @dev 现在这个方法不检查溢出
      function multiply(uint x, uint y) returns (uint z) {
        // 这只是个普通的注释,不会被 natspec 解释
        z = x * y;
      }
    }
    

    @title(标题) 和 @author (作者)很直接了.
    @notice (须知)向 用户 解释这个方法或者合约是做什么的。
    @dev (开发者) 是向开发者解释更多的细节。
    @param (参数)和 @return (返回) 用来描述这个方法需要传入什么参数以及返回什么值。

    注意你并不需要每次都用上所有的标签,它们都是可选的。不过最少,写下一个 @dev 注释来解释每个方法是做什么的。


    7.总结

    这节课我们了解了ERC721标准,它与ERC20不一样的是它的每个代币都是唯一切不可分割的,只能以整个单位进行交易。并且通过ERC721标准怎么实现一个代币合约,如何使用SafeMath来防止溢出和下溢,怎么给智能合约做注释。

    本课程到这里基本上就学完了,对于完全不懂编程的小白老说,可能有点难度。整个课程学下来,对没有时间没有专门学过智能合约的朋友还是有帮助的,能了解智能合约的基本语法,模式和一些技巧。但要自己去编写一个完整的智能合约,还需要自己去搭建自己的开发环境,对智能合约进行深入学习和调试,这些是我们在课后需要去做的。

    祝好!


    系列文章:
    【CryptoZombies|编写区块链游戏学智能合约】Lesson1: 搭建僵尸工厂
    【CryptoZombies|编写区块链游戏学智能合约】Lesson2: 僵尸攻击人类
    【CryptoZombies|编写区块链游戏学智能合约】Lesson3: 搭建僵尸工厂
    【CryptoZombies|编写区块链游戏学智能合约】Lesson4: 僵尸作战系统
    【CryptoZombies|编写区块链游戏学智能合约】Lesson5: ERC721标准和加密收藏品

    相关文章

      网友评论

        本文标题:【CryptoZombies|编写区块链游戏学智能合约】Less

        本文链接:https://www.haomeiwen.com/subject/edvilftx.html