跳到主要内容

🧟‍♂️ 通过CryptoZombies快速上手Solidity

找到一个很好的学习Solidity的网站, 可以通过互动式的教程, 通过写智能合约来学习Solidity, 不用担心环境搭建, IDE配置等上来劝退的问题.

Solidity本身和Java类似, 语言语法不难, 只是生态以及开发工具不同. 开发工具有ganache(搭建测试网络), truffle(cli中部署调试合约), Jest(测试框架)等, 生态中的库有OpenZepplin等. 具体开发流程可参考本文.

第一课: 搭建僵尸工厂

1. 项目概述


我们将创造一个"僵尸工厂", 用它建立一支僵尸部队.

  • 我们的工厂会把我们部队中所有的僵尸保存到数据库中.
  • 工厂会有一个函数能产生新的僵尸.
  • 每个僵尸会有一个随机的独一无二的面孔.

僵尸的外表取决于一个16位的整数, 如: 8356281049284737.

2. 基本数据结构


  • 版本: 一个Solidity文件需要在开头声明编译器版本, 避免编译器更新造成文件破坏.

  • 合约: Solidity代码都包裹在合约里面. 一份合约就是应用的基本模块, 所有的变量和函数都属于一份合约, 它是你所有应用的起点.

    // 声明"代码的版本"
    pragma solidity ^0.4.19;

    // 制造僵尸的合约工厂
    contract ZombieFactory {

    }
  • 状态变量: 状态变量直接写在合约内部一层, 被永久地保存在合约中, 存储在区块链里.

  • 无符号整数: uint全称为unsigned integer, 没有符号的整数, 也就是0以及所有正数. (uint默认为一个可以很大的数, 如果出于某种特殊考虑需要使用数值更小的数, 可以使用uint8等)

    pragma solidity ^0.4.19;

    contract ZombieFactory {
    // DNA的位数是16位.
    uint dnaDigits = 16;
    }
  • 数学运算: 加,减,乘,除,取余数,乘方 === +,-,*,/,%,**.

    pragma solidity ^0.4.19;

    contract ZombieFactory {
    uint dnaDigits = 16;
    // 保证DNA是16位数.
    uint dnaModulus = 10 ** dnaDigits;
    }
  • 结构体: 相对于基本类型, 生成一个更复杂的有更多属性的数据类型.

    pragma solidity ^0.4.19;

    contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    // 一个Zombie类型有名字和DNA.
    struct Zombie {
    string name;
    uint dna;
    }
    }
  • 数组: 建立一个有顺序的集合, 可以是静态或者动态. 公共数组会自动创建getter方法(不用public就得手动写一个function来打印变量, 用了就可以直接获取变量).

    pragma solidity ^0.4.19;

    contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    struct Zombie {
    string name;
    uint dna;
    }
    // zombies数组是动态的, 可以变换长度; 是公共的, 自动创建getter方法; 里面的每个元素都是Zombie类型.
    Zombie[] public zombies;
    }

3. 函数的基本操作


  • 函数: 不变的输入就对应不变的输出. 参数按习惯以_开头.

    pragma solidity ^0.4.19;

    contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    struct Zombie {
    string name;
    uint dna;
    }
    Zombie[] public zombies;
    // 用于创建僵尸的方程, 输入参数为名字和DNA.
    function createZombie(string _name, uint _dna) {
    }
    }
  • 创建新的实例, 操作数组

    pragma solidity ^0.4.19;

    contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    struct Zombie {
    string name;
    uint dna;
    }
    Zombie[] public zombies;
    function createZombie(string _name, uint _dna) {
    // 在zombies状态变量里添加一个新的Zombie(参数来自于方程参数)
    zombies.push(Zombie(_name, _dna));
    }
    }
  • 私有/公共函数: Solidity中函数默认为公共, 任何一方都可以调用, 容易会有安全隐患. 私有函数是只有我们合约中的其它函数才能够调用, 名字一般以_开始.

    pragma solidity ^0.4.19;

    contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    struct Zombie {
    string name;
    uint dna;
    }
    Zombie[] public zombies;
    // private要放在变量名后面.
    function _createZombie(string _name, uint _dna) private {
    zombies.push(Zombie(_name, _dna));
    }
    }
  • 函数更多属性: 可以通过returns (uint)定义返回值; 通过修饰符view表示函数只读取数据; 通过修饰符pure表示是个不读取数据的纯函数. 不需要刻意记忆, IDE会提示.

    pragma solidity ^0.4.19;

    contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    struct Zombie {
    string name;
    uint dna;
    }
    Zombie[] public zombies;
    function _createZombie(string _name, uint _dna) private {
    zombies.push(Zombie(_name, _dna));
    }
    // 定义一个生成随机DNA的函数, 它是个私有函数, 只读取数据, 返回一个无符号整数.
    function _generateRandomDna(string _str) private view returns (uint) {
    }
    }
  • 生成随机数: keccak256()函数可以通过输入的字符串返回16进制随机数(当然不太安全).

  • 类型转换: 由于keccak256返回的是16进制随机数, 所以我们需要将类型转化成我们可以处理的类型.

    pragma solidity ^0.4.19;

    contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    struct Zombie {
    string name;
    uint dna;
    }
    Zombie[] public zombies;
    function _createZombie(string _name, uint _dna) private {
    zombies.push(Zombie(_name, _dna));
    }
    function _generateRandomDna(string _str) private view returns (uint) {
    // 生成uint类型的随机数.
    uint rand = uint(keccak256(_str));
    // 取余数保证是16位.
    return rand % dnaModulus;
    }
    }
  • 创建供外部调用的接口

    pragma solidity ^0.4.19;

    contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    struct Zombie {
    string name;
    uint dna;
    }
    Zombie[] public zombies;
    function _createZombie(string _name, uint _dna) private {
    zombies.push(Zombie(_name, _dna));
    }
    function _generateRandomDna(string _str) private view returns (uint) {
    uint rand = uint(keccak256(_str));
    return rand % dnaModulus;
    }

    // 创建一个公共函数, 调用其他函数.
    function createRandomZombie(string _name) public {
    uint randDna = _generateRandomDna(_name);
    _createZombie(_name, randDna);
    }
    }

4.事件与外部调用


  • 事件: 合约和区块链通讯的一种机制. 前端应用“监听”某些事件,并做出反应.
pragma solidity ^0.4.19;

contract ZombieFactory {
// 创建事件.
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function _createZombie(string _name, uint _dna) private {
// 返回出的id是Zombie在zombies中的索引.
uint id = zombies.push(Zombie(_name, _dna)) - 1;
// 将数据传入到事件中, 触发事件.
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
  • Web3.js: 通过Web3.js, 我们可以从前端调用合约的方法等.

第二课: 僵尸与人类生成新僵尸

1. 项目概述


僵尸吃了人类以后, 可以随机变成新的僵尸. 我们在本课中会尝试实现这么一个"融合"的功能.

2. 映射,地址,Msg.sender


  • 映射: 本质上是存储和查找数据所用的键-值对. 类似Python中的字典.

  • 地址: 类似银行账号, 应用账户, 作为僵尸主人的ID.

    pragma solidity ^0.4.19;

    contract ZombieFactory {
    event NewZombie(uint zombieId, string name, uint dna);
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    struct Zombie {
    string name;
    uint dna;
    }
    Zombie[] public zombies;
    // 创建两个映射, 规定键和值的类型.
    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;
    function _createZombie(string _name, uint _dna) private {
    uint id = zombies.push(Zombie(_name, _dna)) - 1;
    NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
    uint rand = uint(keccak256(_str));
    return rand % dnaModulus;
    }
    function createRandomZombie(string _name) public {
    uint randDna = _generateRandomDna(_name);
    _createZombie(_name, randDna);
    }
    }
  • msg.sender: 在Solidity中, 有一些全局变量可以被所有函数调用. 其中一个就是msg.sender, 它指的是当前调用者(或智能合约)的address.

    pragma solidity ^0.4.19;

    contract ZombieFactory {
    event NewZombie(uint zombieId, string name, uint dna);
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    struct Zombie {
    string name;
    uint dna;
    }
    Zombie[] public zombies;
    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;
    function _createZombie(string _name, uint _dna) private {
    uint id = zombies.push(Zombie(_name, _dna)) - 1;
    // 在mapping中加入/更新键值对
    zombieToOwner[id] = msg.sender;
    ownerZombieCount[msg.sender]++;
    NewZombie(id, _name, _dna);
    }
    function _generateRandomDna(string _str) private view returns (uint) {
    uint rand = uint(keccak256(_str));
    return rand % dnaModulus;
    }
    function createRandomZombie(string _name) public {
    uint randDna = _generateRandomDna(_name);
    _createZombie(_name, randDna);
    }
    }

3. require与分割代码


  • require: 使得函数在执行过程中, 当不满足某些条件时抛出错误, 并停止执行.

    ...
    function createRandomZombie(string _name) public {
    // 要求只能创建一只
    require(ownerZombieCount[msg.sender] == 0);
    uint randDna = _generateRandomDna(_name);
    _createZombie(_name, randDna);
    }
    ...
  • 继承: 当代码过于冗长的时候, 最好将代码和逻辑分拆到多个不同的合约中, 以便于管理. 子合约内可以访问父合约内的public方法.

    ...
    // 继承ZombieFactory
    contract ZombieFeeding is ZombieFactory {

    }
  • import: 当你有多个文件并且想把一个文件导入另一个文件时, 可以使用 import 语句.

    import "./zombiefactory.sol";

    contract ZombieFeeding is ZombieFactory {

    }

4. storage与memory


  • storage与memory: Storage变量是指永久存储在区块链中的变量; Memory变量则是临时的, 当外部函数对某合约调用完成时, 内存型变量即被移除. 大多数时候都用不到这些关键字, 默认情况下Solidity和IDE会自动处理它们, 或者给你一些提醒.

    示例:

    // Sandwich mySandwich = sandwiches[_index];

    // ^ 看上去很直接,不过Solidity将会给出警告
    // 告诉你应该明确在这里定义 `storage` 或者 `memory`。

    // 所以你应该明确定义 `storage`:
    Sandwich storage mySandwich = sandwiches[_index];
    // ...这样 `mySandwich` 是指向合约状态里`sandwiches[_index]`的指针

    // 如果你只想要一个副本,可以使用`memory`:
    Sandwich memory anotherSandwich = sandwiches[_index + 1];

    // 不过你可以这样做, 直接把副本的改动保存回区块链存储:
    sandwiches[_index + 1] = anotherSandwich;
    pragma solidity ^0.4.19;

    import "./zombiefactory.sol";

    contract ZombieFeeding is ZombieFactory {

    function feedAndMultiply(uint _zombieId, uint _targetDna) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    // start here
    }

    }

5. 函数的高级操作

  • 完善ZombieBreeding

    pragma solidity ^0.4.19;

    import "./zombiefactory.sol";

    contract ZombieFeeding is ZombieFactory {
    function feedAndMultiply(uint _zombieId, uint _targetDna) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    // 拿到继承的myZombie的DNA, 并且计算出新Zombie的DNA, 生成Zombie.
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    _createZombie("NoName", newDna);
    }
    }
  • internal:如果某个合约继承自其父合约, 这个合约即可以访问父合约中定义的“内部”函数, private访问不到.

    ...
    // 修改可见性为internal
    function _createZombie(string _name, uint _dna) internal {
    uint id = zombies.push(Zombie(_name, _dna)) - 1;
    zombieToOwner[id] = msg.sender;
    ownerZombieCount[msg.sender]++;
    NewZombie(id, _name, _dna);
    }
    ...
  • external: 与public类似, 只不过这些函数只能在合约之外调用.

6. 嘉然僵尸今天吃什么

  • 嘉然🤤僵尸最喜欢吃的是cryptokitties! 我们现在需要让我们的合约与cryptokitties的合约产生交互, 读取它的数据.

    // 定义Kitty接口, 复制getKitty的内容.
    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 {
    address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
    // 通过给的地址, 初始化kittyContract
    KittyInterface kittyContract = KittyInterface(ckAddress)
  • 处理getKitty接口的多个返回值.

示例:

function multipleReturns() internal returns(uint a, uint b, uint c) {
return (1, 2, 3);
}

function processMultipleReturns() external {
uint a;
uint b;
uint c;
(a, b, c) = multipleReturns();
}

// 或者如果我们只想返回其中一个变量:
function getLastReturnValue() external {
uint c;
(,,c) = multipleReturns();
}
...
function feedOnKitty(uint _zombieId, uint _kittyId) public {
// 定义一个kittyDna.
uint kittyDna;
// 将返回的最后一个值赋给kittyDna.
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
// 传入到feedAndMultiply.
feedAndMultiply(_zombieId, kittyDna);
}
...

第三课: 高级Solidity理论

Code is Law. 你所编译的智能合约会永久存储在以太坊上, 所以安全性和有效性是极其重要的.

1. 可持续性和安全性

  • 我们使用了cryptokitties的合约地址, 但如果cryptokitties没了, 那我们的应用也会无法运行.

    contract ZombieFeeding is ZombieFactory {
    KittyInterface kittyContract;
    // setKittyContractAddress运行时再设定cryptokitties的地址
    function setKittyContractAddress(address _address) external {
    kittyContract = KittyInterface(_address);
    }
  • onlyOwner: 设定external函数为只能由主人调用.

    // 导入openZepplin库的Ownable合约
    import "./ownable.sol";

    contract ZombieFactory is Ownable {
    // modifier onlyOwner() {
    // require(msg.sender == owner);
    // _;
    // }
    // 设置函数为只有主人才能调用.
    function setKittyContractAddress(address _address) external onlyOwner {
    kittyContract = KittyInterface(_address);
    }

未完待续