以太坊構建DApps系列教程(二):構建TNS代幣

在本系列關於使用以太坊構建DApps教程的第1部分中,我們引導大家做了兩個版本的本地區塊鏈進行開發:一個Ganache版本和一個完整的私有PoA版本。

在這一部分中,我們將深入研究並構建我們的TNS代幣:用戶將使用代幣對Story DAO中的提案進行投票。

先決條件

按照上一部分,啓動並運行Ganache版本。或者,如果你沒有從第一部分開始跟蹤,則可以運行任何本地版本的區塊鏈,但請確保你可以使用我們需要的工具連接到它。

我們假設你有一個有效的私有區塊鏈,能夠通過終端應用程序在其控制檯和操作系統終端中輸入命令,或者在Windows上,通過Git Bash,Console,CMD Prompt,Powershell等應用程序輸入命令。

基本依賴

爲了開發我們的應用程序,我們可以使用幾種框架和入門開發包中的一種:Dappeth-utilsPopulusEmbark......等等。但我們會選擇現在的生態系統之王Truffle

使用以下命令安裝它:

npm install -g truffle 

這將使truffle命令無處不在。現在我們可以用truffle init啓動項目。

構建代幣

讓我們直接進入它並構建我們的代幣。它將是一個有點標準的千篇一律的ERC20代幣。(你會看到這篇文章中那個更標準的。)首先,我們將引入一些依賴關係。OpenZeppelin庫是經過實戰考驗的高質量的solidity合約,可用於擴展和構建合約。

npm install openzeppelin-solidity 

接下來,讓我們創建一個新的代幣文件:

truffle create contract TNSToken 

truffle在這裏生成的默認模板有點過時了,所以讓我們更新它:

pragma solidity ^0.4.24;

contract TNStoken {
    constructor() public {

    }
}

到目前爲止,代幣合約的構造函數應該與合約本身一樣被調用,但爲了清楚起見,它被更改爲constructor。它也應該總是有一個修飾符告訴編譯器誰被允許部署和與此合約交互(public意味着每個人)。

SafeMath

我們將在這種情況下使用的唯一Zeppelin合約是他們的SafeMath合約。在Solidity中,我們使用import關鍵字導入合約,而編譯器通常不需要完整路徑,只需要相對的路徑,如下所示:

pragma solidity ^0.4.24;

import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract TNStoken {
    using SafeMath for uint256;
    constructor() public {

    }
}

那麼,什麼是SafeMath?很久以前,由於代碼中的數學問題,出現了1840億比特幣的問題。爲了防止類似於這些問題(並非特別只在以太坊中可能存在這一問題),SafeMath庫仍然存在。當兩個數字具有MAX_INT大小(即操作系統中的最大可能數量)時,將它們相加會使值wrap around重新歸零,就像汽車的里程錶在達到999999公里後重置爲0。所以SafeMath庫具有以下功能:

/**
* @dev Adds two numbers, throws on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256 c) {
  c = a + b;
  assert(c >= a);
  return c;
}

此函數可以防止此問題:它檢查兩個數字的總和是否仍然大於兩個操作數中的每一個。

雖然在撰寫Solidity合約時犯下如此愚蠢的錯誤並不容易,但保持安全比抱歉更好。

通過using SafeMath for uint256,我們用這些“安全”版本替換Solidity(256bit unsigned - aka positive-only - whole numbers)中的標準uint256數字。而不是像這樣求和數:sum=someBigNumber+someBiggerNumber,我們將這樣求和:sum=someBigNumber.add(someBiggerNumber),從而在我們的計算中是安全的。

來自Scratch的ERC20

我們的數學計算安全了,我們可以創建我們的代幣。

ERC20是一個定義明確的標準,所以作爲參考,我們將它添加到合約中。在這裏閱讀代幣標準 。

所以ERC20代幣應該具有的功能是:

pragma solidity ^0.4.24;

import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract ERC20 {
    function totalSupply() public view returns (uint256);
    function balanceOf(address who) public view returns (uint256);
    function transfer(address to, uint256 value) public returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    function allowance(address owner, address spender) public view returns (uint256);
    function transferFrom(address from, address to, uint256 value) public returns (bool);
    function approve(address spender, uint256 value) public returns (bool);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

contract TNStoken {

    using SafeMath for uint256;

    constructor() public {

    }
}

這可能看起來很複雜,但實際上非常簡單。這是我們代幣需要具有的函數的“目錄”,我們將逐個構建它們,解釋每個函數的含義。考慮上面的代幣接口。在創建Story DAO應用程序時,我們將看到它如何以及爲何有用。

基本餘額

開始吧。代幣實際上只是以太坊區塊鏈中的“電子表格”,如下所示:

Name Amount
Bruno 4000
Joe 5000
Anne 0
Mike 300

所以讓我們創建一個mapping,它基本上就像合約中的電子表格:

mapping(address => uint256) balances; 

根據上面的接口,這需要伴隨一個balanceOf函數,它可以讀取此表:

function balanceOf(address _owner) public view returns (uint256) {
    return balances[_owner];
}

函數balanceOf接受一個參數:_ownerpublic(可以被任何人使用),是一個view函數(意思是它可以自由使用——不需要交易),並返回一個uint256編碼,地址所有者的餘額放在裏面。每個人的代幣餘額都是公開可讀的。

總供應量

知道代幣的總供應量對於其用戶和代幣跟蹤應用程序非常重要,所以讓我們定義一個合約屬性(變量)來跟蹤這個和另一個自由函數來讀取它:

uint256 totalSupply_;
function totalSupply() public view returns (uint256) {
    return totalSupply_;
}

發送代幣

接下來,讓我們確保一些代幣的所有者可以將它們發送給其他人。我們還想知道發送何時發生,因此我們也將定義發送事件。Transfer事件允許我們通過JavaScript監聽區塊鏈中的傳輸,以便我們的應用程序可以知道何時發出這些事件,而不是不斷地手動檢查傳輸是否發生。事件與合約中的變量一起聲明,並使用emit關鍵字發出。我們現在將以下內容添加到合約中:

event Transfer(address indexed from, address indexed to, uint256 value);

function transfer(address _to, uint256 _value) public returns (bool) {
    require(_to != address(0));
    require(_value <= balances[msg.sender]);

    balances[msg.sender] = balances[msg.sender].sub(_value);
    balances[_to] = balances[_to].add(_value);
    emit Transfer(msg.sender, _to, _value);
    return true;
}

此函數接受兩個參數:_to,它是將接收代幣的目標地址,以及value,即代幣的數量。重要的是要記住,value是代幣的最小單位數,而不是整個單位。因此,如果一個代幣被聲明具有10位小數的話,那麼爲了發送一個代幣,你將發送10000000000。這種粒度級別允許我們處理極小數量。

該函數是公共的,這意味着任何人都可以使用它,包括其他合約和用戶,並且如果操作成功則返回true

然後該功能進行一些健全性檢查。首先,它檢查目標地址是否爲空地址。換句話說,不得將代幣必須正常發送。接下來,它通過比較它們的餘額(balances[msg.sender] )和傳入的發送值來檢查發件人是否甚至被允許發送這麼多代幣。如果這些檢查中的任何一個失敗,該函數將拒絕該交易並失敗。它將退還所發送的任何代幣,但是在此之前用於執行該功能的gas將被花費。

接下來的兩行從發件人的餘額中減去代幣數量,並將該金額添加到目的地餘額中。然後使用emit事件,並傳入一些值:發件人,收件人和金額。現在,任何訂閱了此合約上的發送事件的客戶都將收到此事件的通知。

好的,現在我們的代幣持有者可以發送代幣。信不信由你,這就是基本代幣所需要的一切。但我們已經要超越了這一點,並增加了一些功能。

津貼

有時可能會允許第三方退出其他帳戶的餘額。這對於可能促進遊戲內購買,去中心化交易等的遊戲應用非常有用。我們通過構建一個名爲allowance的多維mapping實現這一點,該mapping存儲了所有這些權限。我們添加以下內容:

mapping (address => mapping (address => uint256)) internal allowed;
event Approval(address indexed owner, address indexed spender, uint256 value);

這個事件就在那裏,以便應用程序可以知道有人預先批准了其他人的餘額支出,一個有用的功能,以及標準的一部分。

映射將地址與另一個映射相結合,該映射將地址與數字組合在一起,它基本上形成了一個像這樣的電子表格:

所以Bob的餘額可能由Mary支付,最多可達1000個代幣,Billy最多可達50個代幣。Bob可以將Mary的餘額花費750代幣。Billy的餘額最多可以由Mary花費300個,而Joe花費1500。

鑑於此映射是internal映射,它只能由此合約中的函數和使用此合約作爲基礎的合約使用。

要批准其他人從你的帳戶中扣款,你可以使用允許使用代幣的人的地址,允許他們支付的金額以及你發出Approval事件的功能來調用approve功能:

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

我們還需要一種方法來讀取用戶可以從其他用戶的帳戶中花費多少:

function allowance(address _owner, address _spender) public view returns (uint256) {
    return allowed[_owner][_spender];
}

所以它是另一個read only函數(view),這意味着它可以自由執行。它只是讀取剩餘的可提取餘額。

那麼如何爲別人發送?使用新的transferFrom功能:

function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
    require(_to != address(0));
    require(_value <= balances[_from]);
    require(_value <= allowed[_from][msg.sender]);

    balances[_from] = balances[_from].sub(_value);
    balances[_to] = balances[_to].add(_value);
    allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
    emit Transfer(_from, _to, _value);
    return true;
}

和以前一樣,有健全性檢查:目標地址不能是空地址,因此不要將代幣發送到不存在的地方。發送的值還需要不僅小於或等於發送值當前帳戶的餘額,而且還需要小於或等於消息發送者(發起此交易的地址)仍然允許爲他們花費的餘額。

接下來,更新餘額並使允許的餘額與發出有關發送事件之前的餘額同步。

注意:代幣持有者可以在不更新allowed映射的情況下allowed代幣。如果代幣持有者使用transfer手動發送代幣,則會發生這種情況。在這種情況下,持有人的代幣可能比第三方可以支付的額外費用少。

通過批准和許可,我們還可以創建讓代幣持有者增加或減少某人津貼的功能,而不是完全覆蓋該值。嘗試將此作爲練習,然後參考下面的源代碼以獲得解決方案。

function increaseApproval(address _spender, uint _addedValue) public returns (bool) {
    allowed[msg.sender][_spender] = (
    allowed[msg.sender][_spender].add(_addedValue));
    emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
    return true;
}

function decreaseApproval(address _spender, uint _subtractedValue) public returns (bool) {
    uint oldValue = allowed[msg.sender][_spender];
    if (_subtractedValue > oldValue) {
        allowed[msg.sender][_spender] = 0;
    } else {
        allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
    }
    emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
    return true;
}

構造函數

到目前爲止,我們只是建立了一個代幣“合約”。但是這個標記是什麼?它叫什麼?它有多少位小數?我們如何使用它?

在一開始,我們定義了一個constructor函數。現在,讓我們完成它的主體並添加屬性namesymboldecimals

string public name;
string public symbol;
uint8 public decimals;

constructor(string _name, string _symbol, uint8 _decimals, uint256 _totalSupply) public {
    name = _name;
    symbol = _symbol;
    decimals = _decimals;
    totalSupply_ = _totalSupply;
}

這樣做可以讓我們稍後重複使用同一類型的其他代幣。但是,當我們確切知道我們正在構建的內容時,讓我們對這些值進行硬編碼:

string public name;
string public symbol;
uint8 public decimals;

constructor() public {
    name = "The Neverending Story Token;
    symbol = "TNS";
    decimals = 18;
    totalSupply_ = 100 * 10**6 * 10**18;
}

顯示代幣信息時,各種以太坊工具和平臺會讀取這些詳細信息。將合約部署到以太坊網絡時會自動調用構造函數,因此這些值將在部署時自動配置。

關於totalSupply_ = 100*10**6*10**18,這句話只是讓人們更容易閱讀數字的一種方式。由於以太坊中的所有發送都是使用最小的以太單位或代幣(包括小數)完成的,因此最小單位是小數點後18位小數。這就是說單個TNS代幣爲1*10**18*。此外,我們想要1億,所以100*10**6100*10*10*10*10*10*10。這使得數字比100000000000000000000000000更易讀。

替代開發方案

或者,我們也可以擴展Zeppelin合約,修改一些屬性,然後我們就擁有代幣了。這就是大多數人所做的,但在處理可能數百萬其他人的錢的軟件時,我個人傾向於想知道我在代碼中的確切內容,因此盲目代碼重用在我的個人情況下是要最小化的。

pragma solidity ^0.4.24;

import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";
import "../node_modules/openzeppelin-solidity/contracts/token/ERC827/ERC20Token.sol";

contract TNStoken is ERC20Token {
    using SafeMath for uint256;

    string public name;
    string public symbol;
    uint8 public decimals;
    uint256 totalSupply_;

    constructor() public {
        name = "The Neverending Story Token";
        symbol = "TNS";
        decimals = 18;
        totalSupply_ = 100 * 10**6 * 10**18;
    }
}

在這種情況下,我們使用is符號來聲明我們的代幣是ERC20Token。這使得我們的代幣擴展了ERC20合約,後者又擴展了StandardToken,等等......

無論哪種方式,我們的代幣現在已準備就緒。但誰得到了多少代幣以及如何開始?

初始餘額

讓我們給合約的製造者所有的代幣。否則,代幣將不會發送給任何人。通過在其末尾添加以下行來更新constructor

balances[msg.sender] = totalSupply_; 

代幣鎖定

看到我們打算使用代幣作爲投票權(即你在投票期間鎖定了多少代幣代表你的投票有多強大),我們需要一種方法來防止用戶在投票後發送它們,否則我們的DAO將容易受到Sybil攻擊的影響——擁有一百萬個代幣的個人可以註冊100個地址,並通過將它們發送到不同的地址並使用新地址重新投票來獲得1億個代幣的投票權。因此,我們將阻止發送與一個人投票額完全一樣多的代幣,對每個提案的每次投票都是累積的。這是我們在本文開頭提到的扭曲。讓我們在合約中添加以下事件:

event Locked(address indexed owner, uint256 indexed amount);

然後讓我們添加鎖定方法:

function increaseLockedAmount(address _owner, uint256 _amount) onlyOwner public returns (uint256) {
    uint256 lockingAmount = locked[_owner].add(_amount);
    require(balanceOf(_owner) >= lockingAmount, "Locking amount must not exceed balance");
    locked[_owner] = lockingAmount;
    emit Locked(_owner, lockingAmount);
    return lockingAmount;
}

function decreaseLockedAmount(address _owner, uint256 _amount) onlyOwner public returns (uint256) {
    uint256 amt = _amount;
    require(locked[_owner] > 0, "Cannot go negative. Already at 0 locked tokens.");
    if (amt > locked[_owner]) {
        amt = locked[_owner];
    }
    uint256 lockingAmount = locked[_owner].sub(amt);
    locked[_owner] = lockingAmount;
    emit Locked(_owner, lockingAmount);
    return lockingAmount;
}

每種方法都確保不會鎖定或解鎖非法金額,然後在更改給定地址的鎖定金額後發出事件。每個函數還返回現在爲此用戶鎖定的新金額。但這仍然不能阻止發送。讓我們修改transfertransferFrom

function transfer(address _to, uint256 _value) public returns (bool) {
    require(_to != address(0));
    require(_value <= balances[msg.sender] - locked[msg.sender]); // <-- THIS LINE IS DIFFERENT
    // ...

function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
    require(_to != address(0));
    require(_value <= balances[_from] - locked[_from]);
    require(_value <= allowed[_from][msg.sender] - locked[_from]); // <-- THIS LINE IS DIFFERENT
    // ...

最後,我們需要知道爲用戶鎖定或解鎖了多少代幣:

function getLockedAmount(address _owner) view public returns (uint256) {
    return locked[_owner];
}

function getUnlockedAmount(address _owner) view public returns (uint256) {
    return balances[_owner].sub(locked[_owner]);
}

就是這樣:我們的代幣現在可以從外部鎖定,但只能由代幣合約的所有者鎖定(這將是我們將在即將到來的教程中構建的Story DAO)。讓我們將代幣合約設爲Ownable,即允許它擁有一個所有者。使用import "../node_modules/openzeppelin-solidity/contracts/ownership/Ownable.sol"導入;然後更改此行:

 contract StoryDao { 

......是這樣的:

 contract StoryDao is Ownable { 

完整代碼

此時帶有自定義函數註釋的代幣的完整代碼見文末所示。

結論

這部分幫助我們構建了一個基本代幣,我們將在The Neverending Story中將其用作參與/共享代幣。雖然代幣具有效用,但它的定義是作爲一種資產來控制更大的體量的安全代幣。注意區別

在本系列的下一部分中,我們將學習如何編譯,部署和測試此代幣。

======================================================================

分享一些以太坊、EOS、比特幣等區塊鏈相關的交互式在線編程實戰教程:

  • java以太坊開發教程,主要是針對java和android程序員進行區塊鏈以太坊開發的web3j詳解。
  • python以太坊,主要是針對python工程師使用web3.py進行區塊鏈以太坊開發的詳解。
  • php以太坊,主要是介紹使用php進行智能合約開發交互,進行賬號創建、交易、轉賬、代幣開發以及過濾器和交易等內容。
  • 以太坊入門教程,主要介紹智能合約與dapp應用開發,適合入門。
  • 以太坊開發進階教程,主要是介紹使用node.js、mongodb、區塊鏈、ipfs實現去中心化電商DApp實戰,適合進階。
  • C#以太坊,主要講解如何使用C#開發基於.Net的以太坊應用,包括賬戶管理、狀態與交易、智能合約開發與交互、過濾器和交易等。
  • EOS教程,本課程幫助你快速入門EOS區塊鏈去中心化應用的開發,內容涵蓋EOS工具鏈、賬戶與錢包、發行代幣、智能合約開發與部署、使用代碼與智能合約交互等核心知識點,最後綜合運用各知識點完成一個便籤DApp的開發。
  • java比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在Java代碼中集成比特幣支持功能,例如創建地址、管理錢包、構造裸交易等,是Java工程師不可多得的比特幣開發學習課程。
  • php比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在Php代碼中集成比特幣支持功能,例如創建地址、管理錢包、構造裸交易等,是Php工程師不可多得的比特幣開發學習課程。
  • tendermint區塊鏈開發詳解,本課程適合希望使用tendermint進行區塊鏈開發的工程師,課程內容即包括tendermint應用開發模型中的核心概念,例如ABCI接口、默克爾樹、多版本狀態庫等,也包括代幣發行等豐富的實操代碼,是go語言工程師快速入門區塊鏈開發的最佳選擇。

匯智網原創翻譯,轉載請標明出處。這裏是原文以太坊構建DApps系列教程(二):構建TNS代幣

完整代碼:

pragma solidity ^0.4.24;

import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";
import "../node_modules/openzeppelin-solidity/contracts/ownership/Ownable.sol";

contract TNStoken is Ownable {

    using SafeMath for uint256;

    mapping(address => uint256) balances;
    mapping(address => uint256) locked;
    mapping (address => mapping (address => uint256)) internal allowed;
    uint256 totalSupply_;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    event Locked(address indexed owner, uint256 indexed amount);

    string public name;
    string public symbol;
    uint8 public decimals;

    constructor() public {
        name = "The Neverending Story Token";
        symbol = "TNS";
        decimals = 18;
        totalSupply_ = 100 * 10**6 * 10**18;
        balances[msg.sender] = totalSupply_;
    }

    /**
    @dev _owner will be prevented from sending _amount of tokens. Anything
beyond this amount will be spendable.
    */
    function increaseLockedAmount(address _owner, uint256 _amount) public onlyOwner returns (uint256) {
        uint256 lockingAmount = locked[_owner].add(_amount);
        require(balanceOf(_owner) >= lockingAmount, "Locking amount must not exceed balance");
        locked[_owner] = lockingAmount;
        emit Locked(_owner, lockingAmount);
        return lockingAmount;
    }

    /**
    @dev _owner will be allowed to send _amount of tokens again. Anything
remaining locked will still not be spendable. If the _amount is greater
than the locked amount, the locked amount is zeroed out. Cannot be neg.
    */
    function decreaseLockedAmount(address _owner, uint256 _amount) public onlyOwner returns (uint256) {
        uint256 amt = _amount;
        require(locked[_owner] > 0, "Cannot go negative. Already at 0 locked tokens.");
        if (amt > locked[_owner]) {
            amt = locked[_owner];
        }
        uint256 lockingAmount = locked[_owner].sub(amt);
        locked[_owner] = lockingAmount;
        emit Locked(_owner, lockingAmount);
        return lockingAmount;
    }

    function transfer(address _to, uint256 _value) public returns (bool) {
        require(_to != address(0));
        require(_value <= balances[msg.sender] - locked[msg.sender]);

        balances[msg.sender] = balances[msg.sender].sub(_value);
        balances[_to] = balances[_to].add(_value);
        emit Transfer(msg.sender, _to, _value);
        return true;
    }

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

    function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
        require(_to != address(0));
        require(_value <= balances[_from] - locked[_from]);
        require(_value <= allowed[_from][msg.sender] - locked[_from]);

        balances[_from] = balances[_from].sub(_value);
        balances[_to] = balances[_to].add(_value);
        allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
        emit Transfer(_from, _to, _value);
        return true;
    }

    function increaseApproval(address _spender, uint _addedValue) public returns (bool) {
        allowed[msg.sender][_spender] = (
        allowed[msg.sender][_spender].add(_addedValue));
        emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
        return true;
    }

    function decreaseApproval(address _spender, uint _subtractedValue) public returns (bool) {
        uint oldValue = allowed[msg.sender][_spender];
        if (_subtractedValue > oldValue) {
            allowed[msg.sender][_spender] = 0;
        } else {
            allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
        }
        emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
        return true;
    }

    /**
    @dev Returns number of tokens the address is still prevented from using
    */
    function getLockedAmount(address _owner) public view returns (uint256) {
        return locked[_owner];
    }

    /**
    @dev Returns number of tokens the address is allowed to send
    */
    function getUnlockedAmount(address _owner) public view returns (uint256) {
        return balances[_owner].sub(locked[_owner]);
    }

    function balanceOf(address _owner) public view returns (uint256) {
        return balances[_owner];
    }

    function totalSupply() public view returns (uint256) {
        return totalSupply_;
    }

    function allowance(address _owner, address _spender) public view returns (uint256) {
        return allowed[_owner][_spender];
    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章