阅读 624

以太坊官方 Token 代码详解

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

pragma solidity ^0.4.16;
复制代码

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

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

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

contract TokenERC20 {}
复制代码

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

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

我们分别声明了 Token 的全称、符号、最小单位、发行量,它们均被声明成 public ,所以我们可以在部署合约的时候对它们进行指定。

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的几次方)。

然后我们通过 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

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