Home | 簡體中文 | 繁體中文 | 雜文 | 打賞(Donations) | 雲棲社區 | OSChina 博客 | Facebook | Linkedin | 知乎專欄 | Github | Search | About

33.6. 使用代幣替代傳統積分系統

首先我們使用代幣是為了取代傳統的積分機制。因為代幣的“幣”特性能夠實現流通,交易等等,實現一個閉環生態系統。而傳統的積分只能內部使用,無法流通,外接不承認加分的價值,積分無法交易,流通。

其次我們並非是為了ICO炒作,然後割韭菜,代幣取代積分是剛性需求。

這裡假設您已經看我我之前寫的文章,知道什麼是代幣,並且能用錢包部署代幣合約。例如現在合約已經部署到主網,已經可以使用錢包轉賬代幣,甚至代幣已經上了交易所,接下來我們要做什麼呢?

接下來的工作是將代幣和網站或者手機App打通,只有將代幣整合到現有業務場景中代幣才有意義。

33.6.1. 規劃

33.6.1.1. 賬號規劃

我認為我們需要下面幾種角色的賬號

  • 代幣發行賬號,負責發行代幣,管理代幣

  • 收款賬號,用戶代幣流通中的收款,這個賬號應該由財務人員負責,相當於企業對公賬號。

  • 交易所賬號,用來對接交易所

  • 用戶賬號,普用戶的賬號,依賴接受代幣,消費代幣,交易代幣。

33.6.1.2. 日誌規劃

日誌

  1. 開戶日誌

  2. 綁定日誌

  3. 送幣日誌

  4. 商城日誌,代幣轉賬日誌

33.6.1.3. 監控規劃

監控

監控內容

  1. 額度監控,監控賬號額度變化

  2. 交易監控,監控任何一筆交易

  3. 異常監控

33.6.1.4. 代幣構成規劃

例如總量發行1億,1億代幣怎樣構成的?

代幣構成

  1. 1000萬是空投贈送給 XXX 個用戶

  2. 3000萬作為福利發給公司員工

  3. 3000萬放在交易

  4. 3000萬放在網站流通

33.6.2. 實施步驟

如果着手一個遊戲項目上鏈,我們需要怎麼做呢?

上鏈步驟

  • 收集需求,收集公司的內部上鏈需求,聽取所有部門的建議和訴求。

    收集內容例如,代幣發行量多少?代幣小數位數,代幣名稱,是否會增發,是否需要凍結,代幣怎樣流通,怎樣回收

    Dapp 的 UI 設計,各種功能流程

  • 分析需求,因為需求來自各種部門,各種崗位等等,他們不一定從事需求分析工作,所以我們需求對他們的需求過濾,分析,然後給出初步的PRD文檔(產品需求文檔)

  • 根據收集的需求設計合約和Dapp

    根據需求設計Dapp

    系統架構設計,軟件架構設計,技術選型;需要考慮擴展性,靈活性,並發設計,數據安全,部署,後期監控,各種埋點(統計、監控)

  • 準備環境,我們需要三個環境,開發,測試,生產(運營)。

  • 項目啟動

    運維部門準備環境,開始建設監控系統

    開發部門開發合約和Dapp

    測試部門準備測試用例,測試環境

  • 測試

    Alpha 階段,將合約部署到測試環境,測試合約的每個函數的工作邏輯,確保無誤。因為合約一旦部署將不能更改,只能廢棄使用新的合約,所以這個測試步驟必須重視。

    Beta 階段,將測試合約部署到測試網,例如 Ropsten ,可以邀請公司內部測試

  • 部署生產環境

    部署合約,將合約部署到主網

    Dapp 部署到生產環境。

  • 驗收測試,在生產環境做最後驗收測試

  • 代幣上交易所

33.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);
    }
}		
		
		

33.6.4. 打通用戶註冊

這裡我們要實現新用戶在網站上開戶為其創建一個錢包賬號。

對於現有的註冊流程界面部分無需修改,只需在創建賬號邏輯的環節增加一段代碼,去以太坊創建賬號。

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

創建賬號後

		
		註冊成功
-------------------------
用戶名:netkiller
性別:男
...
...
...
錢包賬號:0x627306090abab3a6e1400e9345bc60c78a8bef57	[ 下載備份 ]	

* (提醒)請下載備份您的錢包檔案,並請牢記你的密碼,一旦丟失無法找回。
		
		

點擊下載按鈕後將對應賬號檔案提供給用戶,文章通常在 keystore 類似這種格式 UTC--2018-02-10T09-37-49.558088000Z--5c18a33df2cc41a1beddc91133b8422e89f041b7

有了這個賬號檔案,用戶可以導入到Ethereum Wallet 或者 MetaMask 中。

33.6.5. 現有用戶怎麼處理

新用戶我們可以在用戶註冊的時候為其創建一個錢包賬號。那麼老用戶呢?

老用戶我們提供“綁定”功能,如果用戶已有以太坊賬號,就可以將以太坊賬號與網站賬號做綁定。

通常我們需要一個頁面



當前用戶名:netkiller 
以太坊錢包:___________________________
手機驗證碼:______    [ 獲取驗證碼]

[ 綁定 ] [取消]

填寫錢包賬號,然後點擊獲取驗證碼,然後輸入驗證碼,點擊 “提交” 完成賬號的綁定。

如果你想在平台上提供轉賬等高級操作,你還需要讓用戶上傳 UTC--2018-02-10T09-37-49.558088000Z--5c18a33df2cc41a1beddc91133b8422e89f041b7 檔案,如果採用上傳方案,就不需要綁定了,因為在檔案中(json格式)address 就是賬號。

		
當前用戶名:netkiller 
以太坊錢包:___________________________ [瀏覽]
手機驗證碼:______    [ 獲取驗證碼]

		[ 上傳 ]		[取消]	
		
		

33.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) 的 另一份是空投的。

33.6.7. 賺取代幣

這裡我們舉例幾個場景

發放代幣的方法

  1. 電商平台可以通過活動,訂單量等等條件將代幣發放給用戶

  2. 智能穿戴,例如鞋子,手環,可以根據用戶的運動值這算成代幣,發放給用戶

  3. 遊戲平台,用戶在綫時間,電子競技贏得分數都可以這算成代幣,發放給用戶

33.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);
		
		

33.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);
		
		

33.6.10. 代幣報表

報表是用來展示網站數據變化的表徵圖,這裡只列出與代幣有關的報表。

33.6.10.1. 曾幣報表

用來展示每日,每週,每月.... 贈送,空投的代幣量

33.6.10.2. 積分商城報表

進賬財務數據,每日,每週,每月....

33.6.11. 代幣交易

代幣上交易所後,用戶間就可以了。

我們使用另外一個交易所賬號,參與代幣交易,可以賣幣(回收),買幣(發行)等等操作,實現代幣的閉環流通。