以太坊构建DApps系列教程(二):构建TNS代币

在本系列关于使用以太坊构建DApps教程的第1部分中,咱们引导你们作了两个版本的本地区块链进行开发:一个Ganache版本和一个完整的私有PoA版本。php

在这一部分中,咱们将深刻研究并构建咱们的TNS代币:用户将使用代币对Story DAO中的提案进行投票。html

先决条件

按照上一部分,启动并运行Ganache版本。或者,若是你没有从第一部分开始跟踪,则能够运行任何本地版本的区块链,但请确保你可使用咱们须要的工具链接到它。java

咱们假设你有一个有效的私有区块链,可以经过终端应用程序在其控制台和操做系统终端中输入命令,或者在Windows上,经过Git Bash,Console,CMD Prompt,Powershell等应用程序输入命令。node

基本依赖

为了开发咱们的应用程序,咱们可使用几种框架和入门开发包中的一种:Dappeth-utilsPopulusEmbark......等等。但咱们会选择如今的生态系统之王Trufflepython

使用如下命令安装它:android

npm install -g truffle 
复制代码

这将使truffle命令无处不在。如今咱们能够用truffle init启动项目。git

构建代币

让咱们直接进入它并构建咱们的代币。它将是一个有点标准的千篇一概的ERC20代币。(你会看到这篇文章中那个更标准的。)首先,咱们将引入一些依赖关系。OpenZeppelin库是通过实战考验的高质量的solidity合约,可用于扩展和构建合约。程序员

npm install openzeppelin-solidity 
复制代码

接下来,让咱们建立一个新的代币文件:github

truffle create contract TNSToken 
复制代码

truffle在这里生成的默认模板有点过期了,因此让咱们更新它:web

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];
    }

}
复制代码
相关文章
相关标签/搜索