以太坊官方 Token 代码详解

建议在阅读本文前能对基础的 Solidity 编程语言有必定的了解,由于这方面的资料还很少,因此直接去啃官方文档是最正确的选择(你放心,目前只有英文版的,不过做者我在一些空余时间正在翻译该文档,但愿可以让一些英文基础不太好的读者也能快速走上开发道路上 😆)。git

pragma solidity ^0.4.16;
复制代码

这行代码是全部 Solidity 智能合约的标配开头,旨在告知编译器咱们编写的智能合约使用的 Solidity 语言的版本,防止未来版本的不可兼容性错误。github

interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; }
复制代码

这行代码声明了一个接口 tokenRecipient,能够和继承了该接口的其余合约进行相互调用,这是接口很是重要的特性,其中 interface 是声明接口的关键字。接口内的函数都是未实现的,由于如何实现这些函数并非它要关心的,能够理解为不一样合约间之间的协议,你们共同遵照这个协议,但具体如何细化制定则由各自去实现。接口体内的就是“协议内容”,从代码角度看就是一个未实现的“空”函数。编程

contract TokenERC20 {}
复制代码

咱们正式开始编写智能合约的主体部分了,它定义了一个叫 TokenERC20 的智能合约。bash

string public name;
string public symbol;
uint8 public decimals = 18;
uint256 public totalSupply;
复制代码

咱们分别声明了 Token 的全称、符号、最小单位、发行量,它们均被声明成 public ,因此咱们能够在部署合约的时候对它们进行指定。app

mapping (address => uint256) public balanceOf;
复制代码

咱们声明了一个映射类型的变量 balanceOf,用于存储每一个帐户中对应的余额( Token 数量)。编程语言

mapping (address => mapping (address => uint256)) public allowance;
复制代码

该映射变量则用于存储帐户容许别人转移本身的余额数,简单举个例子就是我有一百万用于慈善事业,我把这一百万的使用权受权给了某慈善基金会,容许他们使用这笔钱(即把这笔钱转移到收款人帐户上),只要他们转移的数目不超过我受权给他们的这一百万,他们想怎么转就怎么转函数

event Transfer(address indexed from, address indexed to, uint256 value);
event Burn(address indexed from, uint256 value);
复制代码

这两行代码是两个事件,也是“空”函数,只须要声明函数名称和入参便可。事件惟一的做用就是当触发该事件时,可以将入参的这些信息传递给客户端,通知它们有事发生,至因而什么事则由不一样的事件来代表,而事情的详情则由入参信息来参考。工具

function TokenERC20(
    uint256 initialSupply,
    string tokenName,
    string tokenSymbol
) public {
    totalSupply = initialSupply * 10 ** uint256(decimals);
    balanceOf[msg.sender] = totalSupply;
    name = tokenName;
    symbol = tokenSymbol;
}
复制代码

该函数是构造函数,每一个合约都有一个这样的函数,且只会在部署合约时触发一次,通常用于初始化一些变量,好比这个构造函数初始化了 Token 的发行量、全称、符号。区块链

其中 initialSupply * 10 ** uint256(decimals) 是进行单位换算,好比咱们发行了100个 Token,但咱们的最小单位是18,因此咱们转帐的时候能够发送10∧-18个 Token,那么咱们在合约内进行转帐统一用最小单位会好不少(其中的 ** 表示幂乘,也就是x的几回方)。ui

而后咱们经过 balanceOf[msg.sender] = totalSupply; 将所有 Token 都转移到了部署合约的帐户下,msg.sender 是一个全局变量,表示当前调用者的帐户地址。

function _transfer(address _from, address _to, uint _value) internal {}
复制代码

这个函数用来进行转帐操做,是一个私有函数(经过使用关键字 internal),入参分别是打款人地址(_from)、收款人地址(_to)以及转帐金额(_value)。下面咱们紧接着分析下这个转帐函数的内部实现:

require(_to != 0x0);
复制代码

首先咱们来看下这个特殊的地址 0x0,能够理解成黑洞,凡是把 Token 转移到这个地址的,都至关于被永久锁定了,不属于任何人了,或许只有上帝才能拿得回来吧😇。

require 关键字表示要执行后面的代码则必须先经过该函数中的条件表达式,即只有当收款人地址不等于 0x0,才能执行接下来的转帐操做,不然就抛出异常。

require(balanceOf[_from] >= _value);
复制代码

咱们大体也能猜出这行代码的意思了,要求打款人的余额得大于他要打款的数额,通俗点就是你要打款100元,那首先你得拿得出这100元💰。

require(balanceOf[_to] + _value > balanceOf[_to]);
复制代码

这行乍一看有点懵,这条件确定成立啊,除非打款数目是个负数 😂,咱们不能要求全部人都那么诚实和遵照规矩,总会有那么几个调皮捣蛋鬼会耍点当心眼。做为程序,尽量去考虑到全部的异常状况,并处理之。

uint previousBalances = balanceOf[_from] + balanceOf[_to];
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
Transfer(_from, _to, _value);
assert(balanceOf[_from] + balanceOf[_to] == previousBalances);
复制代码

这几行咱们放在一块儿讲,先讲第一行和第五行。咱们在作转帐操做前,先记录下他们的余额总和,而后在进行转帐操做后去检验是否他们的余额总和与转帐前仍相等。这是否是有点画蛇添足啊,像是一句废话 😋。这样作主要是保证程序的实际运行结果与预期的必须一致。程序是人写出来的,因此没办法去避免 Bug 的出现。一般使用 assert 是为了在配合使用一些静态分析工具时方便定位出 bug 所在,由于若是这边抛出异常说明代码必定写错了。

assertrequire 功能上都是判断条件表达式并在不知足条件时抛出异常。assert 只被用在内部错误的调试上,是去检验那些具备不变性的结果(好比这边转帐先后的双方余额总和应该是不会变的)。而 require 是被用在能被外部合约调用的那些值上(好比这边检验打款人的余额是否充足等,这些信息都是能被你们查阅的,是公开的)。

Transfer() 这行代码将会向区块链上的所有客户端广播一个事件(好比这边就是:你们注意啦!~打款人xxx向收款人xxx转帐了xxx的钱),至于客户端接收与否那就是客户端本身的事了😏。

多说一句,咱们注意到这个方法是 internal,即外部不可调用。一般咱们对于这些内部方法的取名上采起下划线开头的方式(在写了不少不少行代码后,回头看到这个方法你就很清楚这个方法是个内部方法,这是一条最佳实践~)。

function transfer(address _to, uint256 _value) public {
    _transfer(msg.sender, _to, _value);
}
复制代码

这才是对外开放的转帐方法,从入参上咱们能够看到转帐的打款人必定是调用该方法的帐户。在方法内部经过调用内部方法 _transfer() 来执行转帐操做。

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
    require(_value <= allowance[_from][msg.sender]);
    allowance[_from][msg.sender] -= _value;
    _transfer(_from, _to, _value);
    return true;
}
复制代码

这方法从入参和做用上看简直可怕,任何人都能调用这个方法,并且打款人能够随意指定(你钱多,我指定你为打款人,本身为收款人,疯狂往本身帐户转钱)。固然咱们不能让这样的事发生,咱们从这个方法到底要作什么来看待这个问题。

记住,咱们的例子是官方例子,里面全部的逻辑都是可修改可补充删减的!

咱们但愿能把本身的一部分代理给其余人,让他们去打理(相似银行的理财产品,但没有利息🤣,若是你以为很鸡肋,那么能够修改这个方法,好比想要代理出去的这笔钱是按期存的,且可以有利息的,那就增长代码去实现这部分需求就好啦!)。

要实现这个代理功能,咱们只须要增长一个变量,这个变量存储打款人赋予代理人拥有转帐多少钱。也就是咱们文章开头解释的那个 allowance 变量。

allowance[_from][msg.sender]_from 就是打款人,msg.sender 就是代理人,映射的值就是打理的总余额。接下来的代码就很好理解了,首先咱们须要代理人能打理的总余额足够充足(能支付本次转帐金额),而后从打理总余额中扣除,进行转帐操做,返回成功。

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;
    }
}
复制代码

基本功能和 approve() 方法同样,可是会调用代理人的 receiveApproval() 方法(这但是在调用其余合约的方法呢),固然前提是得代理人合约中实现了这个方法。

要在合约中调用其余合约的公共方法(内部方法你固然没权限去调用的,别想得美),咱们就须要实例化接口,传入其余合约的地址,而后就能够调用接口中声明的全部方法了(再说一遍,前提是其余合约实现了这个方法)。

function burn(uint256 _value) public returns (bool success) {
    require(balanceOf[msg.sender] >= _value);
    balanceOf[msg.sender] -= _value;
    totalSupply -= _value;
    Burn(msg.sender, _value);
    return true;
}
复制代码

个人地盘我作主,一样的,咱们赋予你“烧钱”的权利😌。一旦你调用了这个方法,那么这笔钱就消失了,比转到 0x0 黑洞地址还可怕。第一行,你要烧的钱得是你拿得出的;第二行,从你余额里扣除;第三行,咱们 Token 的总发行量相应减小;第四行,发布烧钱通知(全网都知道我烧了钱,想一想也是装逼的不行啊😂);第五行,返回成功,烧钱成功!

function burnFrom(address _from, uint256 _value) public returns (bool success) {
    require(balanceOf[_from] >= _value);
    require(_value <= allowance[_from][msg.sender]);
    balanceOf[_from] -= _value;
    allowance[_from][msg.sender] -= _value;
    totalSupply -= _value;
    Burn(_from, _value);
    return true;
}
复制代码

既然有代理,那么代理人就有“烧别人钱”的权力了!


官方的 Token 代码讲解就到这里结束,咱们能够根据官方的例子改形成咱们想要的功能 Token,都是可编程的,因此想象空间很大~

最后附上官方完整代码:TokenERC20 · GitHub

欢迎关注公众号:『比特扣』,与我一块儿探索区块链的世界。

相关文章
相关标签/搜索