第05课:ERC-20

第05课:ERC-20 Token 标准详解及代码

本节课主要给大家讲解以太坊代币 ERC-20 标准,解析代币标准中相关的接口及代码实现,带大家快速完成一套代币代码的修改编写。

了解代币发行

代币,英文(Token),翻译成“通证”更贴切一些。如果稍微对区块链有一定的了解,都听说过一两种代币,甚至购买或使用过。最近火爆异常的 EOS 前期就是在以太坊上基于 ERC-20 发布的一套代币。

我们可以通过目前最流行的区块链浏览器之一 Etherscan,来大概了解一下目前市面上的代币情况,访问这个链接,可查看当前在以太坊上发布的代币情况。

enter image description here

通过上图画框的部分可以看到,目前此网址已经检测出 7 万多代币的智能合约,排行居首位的就是最近火爆的 EOS。下面来讲解一下什么是 ERC-20 标准,代币的代码又是如何编写的。

什么是 ERC-20 标准

ERC-20 标准推出于 2015 年 11 月,本质上来说它是一种规范,规定了如果根据这套标准发行代币,需要遵循什么。因此,它也表现出来一种通用的可预测的特性。针对我们开发人员来说它其实就是定义了一些待实现方法的接口(interface),当实现了这套接口的具体方法之后,所有的钱包就可以直接进行兼容,比如 Jaxx、MEW、imToken 等。

从另外一方面来说,Token 合约就是包含了一个对账户地址及其余额的映射的智能合约(Smart Contract)。

ERC-20 接口标准

当要实现一个满足 ERC-20 接口标准的 Token 智能合约时,该合约必须满足以下内容实现。

name

返回当前 Token 的名称,比如:DSToken,可选项。

function name() constant returns (string name)

在智能合约中定义,可通过构造方法传值进行初始化,也可直接在代码中指定:

string public name;

symbol

symbol 就是通常使用的简称,比如:EOS,可选项。

function symbol() constant returns (string symbol)

与 name 一样,可通过构造方法传值进行初始化,也可直接在代码中指定:

string public symbol;

decimals

当前代币支持的最小精度,也就是小数点后多少位,比如此值为 2,表示 1 个 Token 可分割为 100 份。对应以太坊来说,ETH 的此值为 18,也就是说一个 ETH 可以分割为小数点后 18 位的精度。一般情况下,当查询一个 Token 余额时,是按照最小精度的整型来显示的。比如 decimals 为 2,发行一个代币,那么查询到的结果为 100,此值为可选项。

function decimals() constant returns (uint8 decimals)

与 name 和 symbol 一样,可通过构造方法传值进行初始化,也可直接在代码中指定:

uint8 public decimals;

totalSupply

Token 的发型总量,此处需注意这个数量的单位与 decimals 中指定的最小单位一致,注意它们之间的换算。

function totalSupply() constant returns (uint256 totalSupply)

与上面的属性一样,可通过构造方法传值进行初始化,也可直接在代码中指定:

uint256 public totalSupply;

balanceOf

查询指定地址账户余额,返回余额以最小单位计算。

function balanceOf(address _owner) constant returns (uint256 balance)

此账户余额对应智能合约代码中余额的存储,所有的地址与余额之间的关联都是通过此 mapping 进行存储:

mapping (address => uint256) public balanceOf;

transfer

代币转账操作,从执行转账的地址转出指定数量的代币到目标地址,并且必须触发 Transfer 事件。如果执行转账地址没有足够的余额则抛出异常,支持转账金额为 0。

function transfer(address to, uint256 value) public returns (bool);

transferFrom

_from地址发送数量为_value的 token 到_to地址,必须触发 Transfer 事件,主要应用场景为智能合约中对操作账户进行授权,然后达到某个条件时,操作账户可以对被操作账户进行转账操作。如果无权操作则需抛出异常,与 tranfer 相同,可以进行 0 值操作。

function transferFrom(address _from, address _to, uint256 _value) returns (bool success)

approve

设置_spender地址可以从操作此方法的地址那里获得的最高金额,此方法可以多次调用。注意:为了阻止向量攻击,客户端需要确认以这样的方式创建用户接口,即将它们设置为 0,然后将其设置为同一个花费者的另一个值。虽然合同本身不应该强制执行,允许向后兼容以前部署的合同兼容性。

function approve(address _spender, uint256 _value) returns (bool success)

allowance

查询_spender可从_owner提取的金额。

function allowance(address _owner, address _spender) constant returns (uint256 remaining)

Transfer Events

当 Token 被转移的时候必须被触发,包括转移金额为 0。

event Transfer(address indexed _from, address indexed _to, uint256 _value)

Approval Events

当成功调用 approve(address _spender, uint256 _value)后,必须被触发。

event Approval(address indexed _owner, address indexed _spender, uint256 _value)

总结

综上所述我们可以看到一个标准的 ERC-20 代币接口如下:

contract ERC20 {
  function totalSupply() constant returns (uint totalSupply);
  function balanceOf(address _owner) constant returns (uint balance);
  function transfer(address _to, uint _value) returns (bool success);
  function transferFrom(address _from, address _to, uint _value) returns (bool success);
  function approve(address _spender, uint _value) returns (bool success);
  function allowance(address _owner, address _spender) constant returns (uint remaining);
  event Transfer(address indexed _from, address indexed _to, uint _value);
  event Approval(address indexed _owner, address indexed _spender, uint _value);
}

关于 ERC-20 Token 标准就介绍这么多,此协议发布于 2015 年 11 月 19 日,标准的原版位于以太坊的 GitHub 上,最原始版本地址详见这里

实例代码

根据上面的标准讲解,大家已经了解了 ERC-20 的基本规则,下面粘贴一套由本人发布的,已经写入区块链上的代币,对照着上面的规则与下面的代码,做进一步的学习。

pragma solidity ^0.4.18;

/**
 * Math operations with safety checks
 */
contract SafeMath {
  function safeMul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }

  function safeDiv(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b > 0);
    uint256 c = a / b;
    assert(a == b * c + a % b);
    return c;
  }

  function safeSub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function safeAdd(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c>=a && c>=b);
    return c;
  }

}
contract AC is SafeMath{
    string public name;
    string public symbol;
    uint8 public decimals = 18;
    uint256 public totalSupply;
    address public owner;

    /* This creates an array with all balances */
    mapping (address => uint256) public balanceOf;
    mapping (address => uint256) public freezeOf;
    mapping (address => mapping (address => uint256)) public allowance;

    /* This generates a public event on the blockchain that will notify clients */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /* This notifies clients about the amount burnt */
    event Burn(address indexed from, uint256 value);

    /* This notifies clients about the amount frozen */
    event Freeze(address indexed from, uint256 value);

    /* This notifies clients about the amount unfrozen */
    event Unfreeze(address indexed from, uint256 value);

    /* Initializes contract with initial supply tokens to the creator of the contract */
    function AC(
        uint256 initialSupply,
        string tokenName,
        string tokenSymbol,
        address holder)  public{
        totalSupply = initialSupply * 10 ** uint256(decimals); // Update total supply
        balanceOf[holder] = totalSupply;                       // Give the creator all initial tokens
        name = tokenName;                                      // Set the name for display purposes
        symbol = tokenSymbol;                                  // Set the symbol for display purposes
        owner = holder;
    }

    /* Send coins */
    function transfer(address _to, uint256 _value) public{
        require(_to != 0x0);  // Prevent transfer to 0x0 address. Use burn() instead
        require(_value > 0); 
        require(balanceOf[msg.sender] >= _value);           // Check if the sender has enough
        require(balanceOf[_to] + _value >= balanceOf[_to]); // Check for overflows
        balanceOf[msg.sender] = SafeMath.safeSub(balanceOf[msg.sender], _value);                     // Subtract from the sender
        balanceOf[_to] = SafeMath.safeAdd(balanceOf[_to], _value);                            // Add the same to the recipient
        Transfer(msg.sender, _to, _value);                   // Notify anyone listening that this transfer took place
    }

    /* Allow another contract to spend some tokens in your behalf */
    function approve(address _spender, uint256 _value) public
        returns (bool success) {
        require(_value > 0); 
        allowance[msg.sender][_spender] = _value;
        return true;
    }

    /* A contract attempts to get the coins */
    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
        require(_to != 0x0);                                // Prevent transfer to 0x0 address. Use burn() instead
        require(_value > 0); 
        require(balanceOf[_from] >= _value);                 // Check if the sender has enough
        require(balanceOf[_to] + _value >= balanceOf[_to]);  // Check for overflows
        require(_value <= allowance[_from][msg.sender]);     // Check allowance
        balanceOf[_from] = SafeMath.safeSub(balanceOf[_from], _value);                           // Subtract from the sender
        balanceOf[_to] = SafeMath.safeAdd(balanceOf[_to], _value);                             // Add the same to the recipient
        allowance[_from][msg.sender] = SafeMath.safeSub(allowance[_from][msg.sender], _value);
        Transfer(_from, _to, _value);
        return true;
    }

    function burn(uint256 _value) public returns (bool success) {
        require(balanceOf[msg.sender] >= _value);            // Check if the sender has enough
        require(_value > 0); 
        balanceOf[msg.sender] = SafeMath.safeSub(balanceOf[msg.sender], _value);                      // Subtract from the sender
        totalSupply = SafeMath.safeSub(totalSupply,_value);                                // Updates totalSupply
        Burn(msg.sender, _value);
        return true;
    }

    function freeze(uint256 _value) public returns (bool success) {
        require(balanceOf[msg.sender] >= _value);            // Check if the sender has enough
        require(_value > 0); 
        balanceOf[msg.sender] = SafeMath.safeSub(balanceOf[msg.sender], _value);                      // Subtract from the sender
        freezeOf[msg.sender] = SafeMath.safeAdd(freezeOf[msg.sender], _value);                                // Updates totalSupply
        Freeze(msg.sender, _value);
        return true;
    }

    function unfreeze(uint256 _value) public returns (bool success) {
        require(freezeOf[msg.sender] >= _value);            // Check if the sender has enough
        require(_value > 0); 
        freezeOf[msg.sender] = SafeMath.safeSub(freezeOf[msg.sender], _value);                      // Subtract from the sender
        balanceOf[msg.sender] = SafeMath.safeAdd(balanceOf[msg.sender], _value);
        Unfreeze(msg.sender, _value);
        return true;
    }
}

关于此智能合约的代码就不详细进行解释了,有一定编程基础的话基本上能看懂,甚至可以直接修改合约名称即可重新发布一套代币,所有的可变参数都通过构造函数传递。

标准合约扩展

上面介绍了 ERC-20 的标准合约,就像 Java 里面的接口一样,我们要实现接口定义的方法,同时也可以在实现类里面进行其他功能的扩展。上面的合约代码中比标准合约多出了以下功能:

/* This notifies clients about the amount burnt */
event Burn(address indexed from, uint256 value);

/* This notifies clients about the amount frozen */
event Freeze(address indexed from, uint256 value);

/* This notifies clients about the amount unfrozen */
event Unfreeze(address indexed from, uint256 value);

顾名思义,这三个事件分别对应(具体看实现代码):

  • 燃烧操作者账户中指定金额的代币,当燃烧时代币总额也会对应减少;
  • 冻结指定账户指定金额;
  • 接送指定账户指定金额。

实战经验

如果看过几套 Token 代码,你会发现此套代码与一般的代码构造函数有所不同,里面多了以下参数:

owner = holder;

这里的应用场景主要是为了安全起见,比如准备发布一套代币而发布者是技术人员,如果直接将私钥给技术人员或在网络上传输会有安全问题,那么直接通过此参数可以指定此智能合约的 owner,这样就不用担心代币拥有超级权限的 owner 私钥外泄,直接用某一个地址给另外一个地址发布代币,是不是方便很多。

小结

本节课程给大家介绍了 ERC-20 代币的基本标准和示例代码,大家可将此套代码在编辑器或私有链上尝试一下发布和调用,后面的章节会带大家一起来操作。

上一篇
下一篇
目录