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