简单代币开发

代币(Token):

代币单纯从其名字上理解的话,就是一种可以替代通用货币起到交换媒介作用的东西,可以是商场积分,可以是游戏币,也可以是筹码。但是在区块链中,就不完全是那么回事了,区块链中的代币或者说Token通常指的是具有流通性的加密数字权益证明,例如比特币、以太币等数字货币都属于代币

从以上定义可以得知代币的三个要素:

  • 权益证明:一种数字形式存在的权益凭证,代表一种权利,一种固有的内在价值和使用价值
  • 加密:为了防止篡改,保护隐私,不可以复制等
  • 较高的可流通性(去中心化):可以进行交易,兑换通用或法定货币等,之所以要去中心化是因为中心化的代币都存在平台限制及信任问题,所以其流通性就会受到局限

综上,简言之代币的概念大致上就是基于区块链发行的加密货币或者其他类似的东西。通常大多数人或团队在开发区块链项目时,都会考虑发行自己的代币。在最初的时候,我们要发行自己的加密货币得从比特币的源码上改造出来。不过现如今通过以太坊平台,我们能够很方便的开发并发行自己的代币,所以本文将介绍如何基于以太坊开发自己的代币。

本文将使用到两个工具,分别是Remix和MetaMask,如果对这两个工具不太了解的话可以参考我另外两篇文章:

首先我们使用solidity开发一个拥有最基本功能的“代币”demo,以便了解代币最基础的样子,代码如下:

pragma solidity ^0.4.20;

contract SimpleToken {

    // 保存每个地址所拥有的代币数量
    mapping (address => uint256) public balanceOf; 

    // 参数为代币的初始供应量或者说发行数量
    constructor (uint256 initSupply) public {
        // 创建者拥有所有的代币
        balanceOf[msg.sender] = initSupply;
    }

    /**
     * 转移代币
     * 
     * @param _to     接收方的账户地址
     * @param _value  转移的代币数量
     */
    function transfer (address _to, uint256 _value) public {
        // 发送方的账户余额需大于等于转移的代币数量
        require(balanceOf[msg.sender] >= _value);
        // 检查有没有发生溢出,因为数量有可能超过uint256可存储的范围
        require(balanceOf[_to] + _value >= balanceOf[_to]);

        // 减少发送方账户的代币数量
        balanceOf[msg.sender] -= _value;
        // 增加接收方账户的代币数量
        balanceOf[_to] += _value;
    }
}

1.代码编写完成后,在remix上部署合约,首先初始化代币供应量:
如何在以太坊上发行自己的代币

2.查看指定账户所拥有的代币数量:
如何在以太坊上发行自己的代币

3.转移代币到另一个账户地址:
如何在以太坊上发行自己的代币

4.此时该账户地址的代币数量就只剩下100了:
如何在以太坊上发行自己的代币


ERC-20标准简述

在上一小节中,我们实现了一个简单的代币,但就因为简单,所以该代币是极其不完善的,例如没有代币名称、代币符号以及无法控制代币的交易等。

因此以太坊定义了ERC20,ERC20是以太坊定义的一个代币统一标准,该标准描述了我们在实现代币时必须要遵守的协议,如指定代币名称、总量、实现代币交易函数等,只有实现了该协议的代币才能被以太坊钱包支持。

下图是一个实现了ERC20协议的代币:
如何在以太坊上发行自己的代币

实际上ERC20协议是定义了一组标准接口,该协议使得在以太坊上开发的任何代币都能被其他应用程序重用,例如钱包到分散的交易所等,如下:

contract ERC20Interface {
    // 代币名称
    string public constant name = "Token Name";
    // 代币符号或者说简写
    string public constant symbol = "SYM";
    // 代币小数点位数,代币的最小单位, 18表示我们可以拥有 0.0000000000000000001单位个代币
    uint8 public constant decimals = 18;  // 18 is the most common number of decimal places

    // 查询代币的发行总量
    function totalSupply() public constant returns (uint);
    // 查询地址的代币余额
    function balanceOf(address tokenOwner) public constant returns (uint balance);
    // 查询spender允许从tokenOwner上花费的代币数量
    function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
    // 实现代币交易,用于从本账户给某个地址转移代币
    function transfer(address to, uint tokens) public returns (bool success);
    // 允许spender多次从你的账户取款,并且最多可取tokens个,主要用于某些场景下授权委托其他用户从你的账户上花费代币
    function approve(address spender, uint tokens) public returns (bool success);
    // 实现代币用户之间的交易,从一个地址转移代币到另一个地址
    function transferFrom(address from, address to, uint tokens) public returns (bool success);

    // 代币交易时触发的事件,即调用transfer方法时触发
    event Transfer(address indexed from, address indexed to, uint tokens);
    // 允许其他用户从你的账户上花费代币时触发的事件,即调用approve方法时触发
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

ERC-20标准代币的实现

代码如下:

pragma solidity ^0.4.20;

// 定义ERC-20标准接口
contract ERC20Interface {
    // 代币名称
    string public name;
    // 代币符号或者说简写
    string public symbol;
    // 代币小数点位数,代币的最小单位
    uint8 public decimals;
    // 代币的发行总量
    uint public totalSupply;

    // 实现代币交易,用于给某个地址转移代币
    function transfer(address to, uint tokens) public returns (bool success);
    // 实现代币用户之间的交易,从一个地址转移代币到另一个地址
    function transferFrom(address from, address to, uint tokens) public returns (bool success);
    // 允许spender多次从你的账户取款,并且最多可取tokens个,主要用于某些场景下授权委托其他用户从你的账户上花费代币
    function approve(address spender, uint tokens) public returns (bool success);
    // 查询spender允许从tokenOwner上花费的代币数量
    function allowance(address tokenOwner, address spender) public view returns (uint remaining);

    // 代币交易时触发的事件,即调用transfer方法时触发
    event Transfer(address indexed from, address indexed to, uint tokens);
    // 允许其他用户从你的账户上花费代币时触发的事件,即调用approve方法时触发
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

// 实现ERC-20标准接口
contract ERC20Impl is ERC20Interface {
    // 存储每个地址的余额(因为是public的所以会自动生成balanceOf方法)
    mapping (address => uint256) public balanceOf;
    // 存储每个地址可操作的地址及其可操作的金额
    mapping (address => mapping (address => uint256)) internal allowed;

    // 初始化属性
    constructor() public {
        name = "Test Token";
        symbol = "TEST"; 
        decimals = 18;
        totalSupply = 100000000;
        // 初始化该代币的账户会拥有所有的代币
        balanceOf[msg.sender] = totalSupply;
    }

    function transfer(address to, uint tokens) public returns (bool success) {
        // 检验接收者地址是否合法
        require(to != address(0));
        // 检验发送者账户余额是否足够
        require(balanceOf[msg.sender] >= tokens);
        // 检验是否会发生溢出
        require(balanceOf[to] + tokens >= balanceOf[to]);

        // 扣除发送者账户余额
        balanceOf[msg.sender] -= tokens;
        // 增加接收者账户余额
        balanceOf[to] += tokens;

        // 触发相应的事件
        emit Transfer(msg.sender, to, tokens);

                success = true;
    }

    function transferFrom(address from, address to, uint tokens) public returns (bool success) {
        // 检验地址是否合法
        require(to != address(0) && from != address(0));
        // 检验发送者账户余额是否足够
        require(balanceOf[from] >= tokens);
        // 检验操作的金额是否是被允许的
        require(allowed[from][msg.sender] <= tokens);
        // 检验是否会发生溢出
        require(balanceOf[to] + tokens >= balanceOf[to]);

        // 扣除发送者账户余额
        balanceOf[from] -= tokens;
        // 增加接收者账户余额
        balanceOf[to] += tokens;

        // 触发相应的事件
        emit Transfer(from, to, tokens);   

        success = true;
    }

    function approve(address spender, uint tokens) public returns (bool success) {
        allowed[msg.sender][spender] = tokens;
        // 触发相应的事件
        emit Approval(msg.sender, spender, tokens);

        success = true;
    }

    function allowance(address tokenOwner, address spender) public view returns (uint remaining) {
        return allowed[tokenOwner][spender];
    }
}

将以上代码复制到Remix上,编译合约代码:
如何在以太坊上发行自己的代币

部署合约:
如何在以太坊上发行自己的代币

部署成功:
如何在以太坊上发行自己的代币

测试balanceOf方法:
如何在以太坊上发行自己的代币

测试transfer方法:
如何在以太坊上发行自己的代币


在Remix上测试完基本的方法后,我们通过MetaMask在以太坊的测试网络上发行我们的代币。打开MetaMask选择Ropsten测试网络,如下:
如何在以太坊上发行自己的代币

注:如果你没有余额请点击DEPOSIT按钮,然后再点击GET ETHER按钮,会进入一个网站,在该网站中点击request 1 ether from faucet,可以送一些测试网络的以太币给你:
如何在以太坊上发行自己的代币

然后到Remix IDE上刷新一下,此时Remix会自动选择相应的测试网络,注意MetaMask和Remix需要在同一个浏览器上,并且Environment和Account和MetaMask保持一致,如下:
如何在以太坊上发行自己的代币

点击Deploy后,这时MetaMask会弹出一个交易确认框,点CONFIRM:
如何在以太坊上发行自己的代币

待合约部署交易确认之后,复制合约地址:
如何在以太坊上发行自己的代币

打开Metamask界面,打开菜单(Menu),点ADD TOKEN:
如何在以太坊上发行自己的代币

出现如下对话框,选择Custom Token,然后将合约地址粘贴进去:
如何在以太坊上发行自己的代币

点ADD TOKENS:
如何在以太坊上发行自己的代币

这时在MetaMask里就可以看到我们创建的代币了,如下:
如何在以太坊上发行自己的代币

接下来我们测试一下代币的转账功能,选择我们刚刚部署的代币,点击SEND:
如何在以太坊上发行自己的代币

填写转账的相关信息:
如何在以太坊上发行自己的代币

确认转账:
如何在以太坊上发行自己的代币

成功后会生成相应的交易记录:
如何在以太坊上发行自己的代币

如此一来我们就完成了代币的创建和部署(正式网络和测试网络部署方法一样),现在已经可以在Etherscan查询到我们刚刚发行的代币了:
如何在以太坊上发行自己的代币

点击合约地址可以查看到代币的详情
如何在以太坊上发行自己的代币


关于发行总量为0的问题

在上一小节中,我们已经成功在以太坊上发行了我们自己的代币,但许多同学应该也已经产生了一个疑问,为啥明明代币中设置了代币的发行数量,但是发行的数量还是为0呢?其实这也是新手在开发代币时可能会遇到的一个坑,之所以我们看到发行总量为0是因为代币的发行总量的整数位表示并不是totalSupply的值,而是totalSupply除以10的decimals次方。

例如我们将decimals设置为8,那么就需要以代币的发行总量(totalSupply)除以100000000,才能得到其整数。上一小节中合约代码里设置的decimals值为18,代表代币使用的小数位为18位,而totalSupply的值为1亿,自然计算出来就是小于1的小数,而且还是比较小的小数,MetaMask只显示3位小数,所以导致在我们看来代币的发行总量为0。

既然知道是什么原因导致的,那么解决起来就好办了,修改合约的构造器代币如下:

// 初始化属性
constructor() public {
    decimals = 18;
    totalSupply = 100000000 * 10 ** uint256(decimals);
    name = "Test Token";
    symbol = "TEST"; 
    // 初始化该代币的账户会拥有所有的代币
    balanceOf[msg.sender] = totalSupply;
}

修改完成后,依旧按照我们上一小节所描述的流程去发行代币并将代币添加到MetaMask钱包中。此时该代币的发行总量就应该是我们期望的值了,如下:
如何在以太坊上发行自己的代币

我们也可以尝试使用代币进行转账:
如何在以太坊上发行自己的代币

转账成功,该账户的余额也正常扣减了:
如何在以太坊上发行自己的代币

另一个账户也正常收到了这100个代币:
如何在以太坊上发行自己的代币

然后我们再去查看一下代币详情
如何在以太坊上发行自己的代币