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

310 阅读18分钟

在本系列关于使用以太坊构建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];
    }

}