Home | 简体中文 | 繁体中文 | 杂文 | 知乎专栏 | Github | OSChina 博客 | 云社区 | 云栖社区 | Facebook | Linkedin | 视频教程 | 打赏(Donations) | About
知乎专栏多维度架构微信号 netkiller-ebook | QQ群:128659835 请注明“读者”

2.6. 使用代币替代传统积分系统

首先我们使用代币是为了取代传统的积分机制。因为代币的“币”特性能够实现流通,交易等等,实现一个闭环生态系统。而传统的积分只能内部使用,无法流通,外接不承认加分的价值,积分无法交易,流通。

其次我们并非是为了ICO炒作,然后割韭菜,代币取代积分是刚性需求。

这里假设您已经看我我之前写的文章,知道什么是代币,并且能用钱包部署代币合约。例如现在合约已经部署到主网,已经可以使用钱包转账代币,甚至代币已经上了交易所,接下来我们要做什么呢?

接下来的工作是将代币和网站或者手机App打通,只有将代币整合到现有业务场景中代币才有意义。

2.6.1. 规划

2.6.1.1. 账号规划

我认为我们需要下面几种角色的账号

  • 代币发行账号,负责发行代币,管理代币

  • 收款账号,用户代币流通中的收款,这个账号应该由财务人员负责,相当于企业对公账号。

  • 交易所账号,用来对接交易所

  • 用户账号,普用户的账号,依赖接受代币,消费代币,交易代币。

2.6.1.2. 日志规划

日志

  1. 开户日志

  2. 绑定日志

  3. 送币日志

  4. 商城日志,代币转账日志

2.6.1.3. 监控规划

监控

监控内容

  1. 额度监控,监控账号额度变化

  2. 交易监控,监控任何一笔交易

  3. 异常监控

2.6.1.4. 代币构成规划

例如总量发行1亿,1亿代币怎样构成的?

代币构成

  1. 1000万是空投赠送给 XXX 个用户

  2. 3000万作为福利发给公司员工

  3. 3000万放在交易

  4. 3000万放在网站流通

2.6.2. 实施步骤

如果着手一个游戏项目上链,我们需要怎么做呢?

上链步骤

  • 收集需求,收集公司的内部上链需求,听取所有部门的建议和诉求。

    收集内容例如,代币发行量多少?代币小数位数,代币名称,是否会增发,是否需要冻结,代币怎样流通,怎样回收

    Dapp 的 UI 设计,各种功能流程

  • 分析需求,因为需求来自各种部门,各种岗位等等,他们不一定从事需求分析工作,所以我们需求对他们的需求过滤,分析,然后给出初步的PRD文档(产品需求文档)

  • 根据收集的需求设计合约和Dapp

    根据需求设计Dapp

    系统架构设计,软件架构设计,技术选型;需要考虑扩展性,灵活性,并发设计,数据安全,部署,后期监控,各种埋点(统计、监控)

  • 准备环境,我们需要三个环境,开发,测试,生产(运营)。

  • 项目启动

    运维部门准备环境,开始建设监控系统

    开发部门开发合约和Dapp

    测试部门准备测试用例,测试环境

  • 测试

    Alpha 阶段,将合约部署到测试环境,测试合约的每个函数的工作逻辑,确保无误。因为合约一旦部署将不能更改,只能废弃使用新的合约,所以这个测试步骤必须重视。

    Beta 阶段,将测试合约部署到测试网,例如 Ropsten ,可以邀请公司内部测试

  • 部署生产环境

    部署合约,将合约部署到主网

    Dapp 部署到生产环境。

  • 验收测试,在生产环境做最后验收测试

  • 代币上交易所

2.6.3. ERC20 代币合约

合约部署这里加就不介绍了,可以参考《Netkiller Blockchain 手札》

合约地址:https://raw.githubusercontent.com/ibook/TokenERC20/master/contracts/TokenERC20.sol

这个合约提供增发,冻结,所有权转移等等功能。

		
pragma solidity ^0.4.21;

interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; }

contract TokenERC20 {
    address public owner;
    // Public variables of the token
    string public name;
    string public symbol;
    uint8 public decimals = 18;
    // 18 decimals is the strongly suggested default, avoid changing it
    uint256 public totalSupply;

    // This creates an array with all balances
    mapping (address => uint256) public balanceOf;
    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);

    mapping (address => bool) public frozenAccount;
    event FrozenFunds(address target, bool frozen);

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

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }

    /**
     * Internal transfer, only can be called by this contract
     */
    function _transfer(address _from, address _to, uint _value) internal {
        // Prevent transfer to 0x0 address. Use burn() instead
        require(_to != 0x0);
        // Check if the sender has enough
        require(balanceOf[_from] >= _value);
        // Check for overflows
        require(balanceOf[_to] + _value > balanceOf[_to]);
        // Save this for an assertion in the future
        uint previousBalances = balanceOf[_from] + balanceOf[_to];
        // Subtract from the sender
        balanceOf[_from] -= _value;
        // Add the same to the recipient
        balanceOf[_to] += _value;
        Transfer(_from, _to, _value);
        // Asserts are used to use static analysis to find bugs in your code. They should never fail
        assert(balanceOf[_from] + balanceOf[_to] == previousBalances);
    }
    
    function transfer(address _to, uint256 _value) public {
        require(!frozenAccount[msg.sender]);
        _transfer(msg.sender, _to, _value);
    }

    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
        require(!frozenAccount[msg.sender]);
        require(_value <= allowance[_from][msg.sender]);     // Check allowance
        allowance[_from][msg.sender] -= _value;
        _transfer(_from, _to, _value);
        return true;
    }

    function approve(address _spender, uint256 _value) public
        returns (bool success) {
        allowance[msg.sender][_spender] = _value;
        return true;
    }

    function approveAndCall(address _spender, uint256 _value, bytes _extraData)
        public
        returns (bool success) {
        tokenRecipient spender = tokenRecipient(_spender);
        if (approve(_spender, _value)) {
            spender.receiveApproval(msg.sender, _value, this, _extraData);
            return true;
        }
    }

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


    function burnFrom(address _from, uint256 _value) onlyOwner public returns (bool success) {
        require(balanceOf[_from] >= _value);                // Check if the targeted balance is enough
        require(_value <= allowance[_from][msg.sender]);    // Check allowance
        balanceOf[_from] -= _value;                         // Subtract from the targeted balance
        allowance[_from][msg.sender] -= _value;             // Subtract from the sender's allowance
        totalSupply -= _value;                              // Update totalSupply
        Burn(_from, _value);
        return true;
    }


  function transfer(address _to, uint256 _value, bytes _data) public returns (bool) {
    require(_to != address(this));
    transfer(_to, _value);
    require(_to.call(_data));
    return true;
  }

  function transferFrom(address _from, address _to, uint256 _value, bytes _data) public returns (bool) {
    require(_to != address(this));

    transferFrom(_from, _to, _value);

    require(_to.call(_data));
    return true;
  }

  function approve(address _spender, uint256 _value, bytes _data) public returns (bool) {
    require(_spender != address(this));

    approve(_spender, _value);

    require(_spender.call(_data));

    return true;
  }

    
    function transferOwnership(address _owner) onlyOwner public {
        owner = _owner;
    }
    function mintToken(address target, uint256 mintedAmount) public onlyOwner {
        balanceOf[target] += mintedAmount;
        totalSupply += mintedAmount;
        Transfer(0, owner, mintedAmount);
        Transfer(owner, target, mintedAmount);
    }

    function freezeAccount(address target, bool freeze) public onlyOwner {
        frozenAccount[target] = freeze;
        FrozenFunds(target, freeze);
    }
}		
		
		

2.6.4. 打通用户注册

这里我们要实现新用户在网站上开户为其创建一个钱包账号。

对于现有的注册流程界面部分无需修改,只需在创建账号逻辑的环节增加一段代码,去以太坊创建账号。

		
web3.eth.personal.newAccount('!@superpassword').then(console.log);
		
		

创建账号后

		
		注册成功
-------------------------
用户名:netkiller
性别:男
...
...
...
钱包账号:0x627306090abab3a6e1400e9345bc60c78a8bef57	[ 下载备份 ]	

* (提醒)请下载备份您的钱包文件,并请牢记你的密码,一旦丢失无法找回。
		
		

点击下载按钮后将对应账号文件提供给用户,文章通常在 keystore 类似这种格式 UTC--2018-02-10T09-37-49.558088000Z--5c18a33df2cc41a1beddc91133b8422e89f041b7

有了这个账号文件,用户可以导入到Ethereum Wallet 或者 MetaMask 中。

2.6.5. 现有用户怎么处理

新用户我们可以在用户注册的时候为其创建一个钱包账号。那么老用户呢?

老用户我们提供“绑定”功能,如果用户已有以太坊账号,就可以将以太坊账号与网站账号做绑定。

通常我们需要一个页面



当前用户名:netkiller 
以太坊钱包:___________________________
手机验证码:______    [ 获取验证码]

[ 绑定 ] [取消]

填写钱包账号,然后点击获取验证码,然后输入验证码,点击 “提交” 完成账号的绑定。

如果你想在平台上提供转账等高级操作,你还需要让用户上传 UTC--2018-02-10T09-37-49.558088000Z--5c18a33df2cc41a1beddc91133b8422e89f041b7 文件,如果采用上传方案,就不需要绑定了,因为在文件中(json格式)address 就是账号。

		
当前用户名:netkiller 
以太坊钱包:___________________________ [浏览]
手机验证码:______    [ 获取验证码]

		[ 上传 ]		[取消]	
		
		

2.6.6. 赠送代币

对于新开户,或者老用户绑定了钱包。我们通常要意思一下,就是送点币。

送币有两种方式,第一种是转账给用户,缺点是需要花店gas(气),第二种是采用空投方式,就是在用户查询余额的时候送币。

先来说说第一种,转账方式。

		
fs = require('fs');
const Web3 = require('web3');
const web3 = new Web3('http://localhost:8545');
web3.version
const abi = fs.readFileSync('output/TokenERC20.abi', 'utf-8');

const contractAddress = "0x05A97632C197a0496bc939C4e666c2E03Cb95DD4";
const toAddress = "0x2C687bfF93677D69bd20808a36E4BC2999B4767C";

var coinbase;

web3.eth.getCoinbase().then(function (address){
  coinbase = address;
  console.log(address);
});

const contract = new web3.eth.Contract(JSON.parse(abi), contractAddress, { from: coinbase , gas: 100000});

contract.methods.balanceOf('0x5c18a33DF2cc41a1bedDC91133b8422e89f041B7').call().then(console.log).catch(console.error);
contract.methods.balanceOf('0x2C687bfF93677D69bd20808a36E4BC2999B4767C').call().then(console.log).catch(console.error);

web3.eth.personal.unlockAccount(coinbase, "netkiller").then(console.log);
contract.methods.transfer('0x2C687bfF93677D69bd20808a36E4BC2999B4767C', 100).send().then(console.log).catch(console.error);

contract.methods.balanceOf('0x2C687bfF93677D69bd20808a36E4BC2999B4767C').call().then(console.log).catch(console.error);
		
		

第二种是空投币

		
uint totalSupply = 100000000 ether; 	// 总发行量
uint currentTotalAirdrop = 0;    		// 已经空投数量
uint airdrop = 1 ether;        		// 单个账户空投数量

// 存储是否空投过
mapping(address => bool) touched;

// 修改后的balanceOf方法
function balanceOf(address _owner) public view returns (uint256 balance) {    
    if (!touched[_owner] && currentTotalAirdrop < totalSupply) {
        touched[_owner] = true;
        currentTotalAirdrop += airdrop;
        balances[_owner] += airdrop;
    }    
    return balances[_owner];
}
		
		

空投代币省了 Gas,但是带来一个问题,就是实际代币发行量成了 totalSupply * 2 ,因为创建合约的时候代币全部发给了 msg.sender ,空投只能使用增发币,无法去 msg.sender 扣除的空投数量。

空投币不好管理发行量。有一种做法,就是发行的时候分为2分,一份是 coinbase(msg.sender) 的 另一份是空投的。

2.6.7. 赚取代币

这里我们举例几个场景

发放代币的方法

  1. 电商平台可以通过活动,订单量等等条件将代币发放给用户

  2. 智能穿戴,例如鞋子,手环,可以根据用户的运动值这算成代币,发放给用户

  3. 游戏平台,用户在线时间,电子竞技赢得分数都可以这算成代币,发放给用户

2.6.8. 用户登录

第一个界面一定是,请输入用户名和密码,然后提交登录。

登录后进入用户信息页面

		
		用户登录成功
------------------------------
当前用户名:netkiller 
...
...

钱包账号:0x627306090abab3a6e1400e9345bc60c78a8bef57

------------------------------
当前余额: 1000 NEO
		
		

NEO是代币符号,假设叫NEO,这是我的英文名。实际上NEO已经被其他代币使用:(

获取账号余额代码

		
fs = require('fs');
const Web3 = require('web3');
const web3 = new Web3('http://localhost:8545');
const abi = fs.readFileSync('output/TokenERC20.abi', 'utf-8');

const contractAddress = "0x05A97632C197a0496bc939C4e666c2E03Cb95DD4";
const fromAddress = "0x5c18a33DF2cc41a1bedDC91133b8422e89f041B7";	//用户账号
const toAddress = "0x2C687bfF93677D69bd20808a36E4BC2999B4767C";	//收款账号

const contract = new web3.eth.Contract(JSON.parse(abi), contractAddress, { from: fromAddress , gas: 100000});
contract.methods.balanceOf(toAddress).call().then(console.log).catch(console.error);
		
		

2.6.9. 积分商城

这里是消费代币的地方,可以使用代币对兑换礼品,购买物品等等。

用户花出去代币去向应该是,用户收款的财务账号。

		
fs = require('fs');
const Web3 = require('web3');
const web3 = new Web3('http://localhost:8545');
const abi = fs.readFileSync('output/TokenERC20.abi', 'utf-8');

const contractAddress = "0x05A97632C197a0496bc939C4e666c2E03Cb95DD4";
const fromAddress = "0x5c18a33DF2cc41a1bedDC91133b8422e89f041B7";	//用户账号
const toAddress = "0x2C687bfF93677D69bd20808a36E4BC2999B4767C";	//收款账号

const contract = new web3.eth.Contract(JSON.parse(abi), contractAddress, { from: fromAddress , gas: 100000});

web3.eth.personal.unlockAccount(fromAddress, "netkiller").then(console.log);
contract.methods.transfer(toAddress, 10).send().then(console.log).catch(console.error); //花费代币 10
contract.methods.balanceOf(toAddress).call().then(console.log).catch(console.error);
		
		

2.6.10. 代币报表

报表是用来展示网站数据变化的图标,这里只列出与代币有关的报表。

2.6.10.1. 曾币报表

用来展示每日,每周,每月.... 赠送,空投的代币量

2.6.10.2. 积分商城报表

进账财务数据,每日,每周,每月....

2.6.11. 代币交易

代币上交易所后,用户间就可以了。

我们使用另外一个交易所账号,参与代币交易,可以卖币(回收),买币(发行)等等操作,实现代币的闭环流通。