以太坊構建DApps系列教程(二):構建TNS代幣
在本系列關於使用以太坊構建DApps教程的第1部分中,我們引導大家做了兩個版本的本地區塊鏈進行開發:一個Ganache版本和一個完整的私有PoA版本。
在這一部分中,我們將深入研究並構建我們的TNS代幣:使用者將使用代幣對Story DAO中的提案進行投票。
先決條件
按照上一部分,啟動並執行Ganache版本。或者,如果你沒有從第一部分開始跟蹤,則可以執行任何本地版本的區塊鏈,但請確保你可以使用我們需要的工具連線到它。
我們假設你有一個有效的私有區塊鏈,能夠通過終端應用程式在其控制檯和作業系統終端中輸入命令,或者在Windows上,通過Git Bash,Console,CMD Prompt,Powershell等應用程式輸入命令。
基本依賴
為了開發我們的應用程式,我們可以使用幾種框架和入門開發包中的一種:Dapp,eth-utils,Populus,Embark…等等。但我們會選擇現在的生態系統之王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接受一個引數:_owner
是public
(可以被任何人使用),是一個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
函式。現在,讓我們完成它的主體並新增屬性name
,symbol
和decimals
:
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**6
或100*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;
}
每種方法都確保不會鎖定或解鎖非法金額,然後在更改給定地址的鎖定金額後發出事件。每個函式還返回現在為此使用者鎖定的新金額。但這仍然不能阻止傳送。讓我們修改transfer
和transferFrom
:
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];
}
}