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 私钥外泄,直接用某一个地址给另外一个地址发布代币,是不是方便很多。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容