建议在阅读本文前能对基础的 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 所在,因为如果这边抛出异常说明代码一定写错了。
assert
和 require
功能上都是判断条件表达式并在不满足条件时抛出异常。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
欢迎关注公众号:『比特扣』,与我一起探索区块链的世界。