以太坊 ERC20 Token合约代码分析

以太坊ERC20 Token合约代码分析

之前跟着公众号文章在个人私有链上发布过ERC20 的Token合约,当时稀里糊涂的,代码也没有细看,最近在做Solidity代码相关的复习,上次上完培训课之后都有点遗忘了,借此机会,整理一下ERC20 Token相关的知识点,以备以后使用。


0x0 ERC20 标准

EIPs/eip-20.md at master · ethereum/EIPs · GitHub
标准的内容比较简单,英文不算生僻,此处不再全部翻译,着重记录几个点。

合约代码里至少包含以下方法

name

可选,返回token的名称,例如“TedToken”。
function name() view returns (string name)

symbol

可选,返回token的符号,比如“TEC”。
function symbol() view returns (string symbol)

decimals

可选,返回token的精确小数点,通用是18。
function decimals() view returns (uint8 decimals)

totalSupply

全部供应量,也就是发行量。
function totalSupply() view returns (uint256 totalSupply)

balanceOf

查询余额,返回指定address的余额
function balanceOf(address _owner) view returns (uint256 balance)

transfer

发送token,发送指定数目的token到指定账户,必须触发Transfer事件。如果余额不足,必须throw异常。另外,即使转账0个token,也必须向非0转账那样触发Transfer事件。
一般是owner向其他人发送代币。
function transfer(address _to, uint256 _value) returns (bool success)

transferFrom

交易token,从一个地址向另外一个地址转账指定额度的token,必须触发Transfer事件,这个方法可以理解为一个收款流程,允许合约来代表token持有者发送代币。比如,合约可以帮助你向另外一个人发送token或者索要token。前提是token拥有者必须要通过某些机制对这个请求进行确认,比如通过MetaMask进行confirm。否则,执行将失败。
跟transfer一样,即使发送0代币,也要触发Transfer事件。
function transferFrom(address _from, address _to, uint256 _value) returns (bool success)

approve

批准额度,允许一个账户最多能从你的账户你取现指定额度。重复调用时,以最后一次的额度为主。为了防止攻击,最开始这个额度必须设置为0。
function approve(address _spender, uint256 _value) returns (bool success)

allowance

跟approve对应,获得指定用户的批准额度。
function allowance(address _owner, address _spender) view returns (uint256 remaining)

合约代码里至少包含以下事件

Transfer

任何token发送发生时,必须触发该事件,即使是0额度。
当一个token合约创建时,应该触发一个Transfer事件,token的发送方是0x0,也就是说凭空而来的token,简称空气币。
event Transfer(address indexed _from, address indexed _to, uint256 _value)

Approval

当approve被调用时,需要触发该事件。
event Approval(address indexed _owner, address indexed _spender, uint256 _value)

0x1 Token示例源码一

官方源码:Tokens/EIP20.sol at master · ConsenSys/Tokens · GitHub 我的源码:GitHub - xiongwei-git/Tokens: One EIP-20 Token Sample

20180611152870725479152.png
这个示例源码比较简单,包含三个sol文件,分别是接口定义EIP20Interface,Token实现EIP20和Token管理类EIP20Factory。

  • 首先看EIP20Interface,都是上文提到的标准接口和一些事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
contract EIP20Interface {
/* This is a slight change to the ERC20 base standard.
function totalSupply() constant returns (uint256 supply);
is replaced with:
uint256 public totalSupply;
This automatically creates a getter function for the totalSupply.
This is moved to the base contract since public getter functions are not
currently recognised as an implementation of the matching abstract
function by the compiler.
*/
/// total amount of tokens
uint256 public totalSupply;

/// @param _owner The address from which the balance will be retrieved
/// @return The balance
function balanceOf(address _owner) public view returns (uint256 balance);

/// @notice send `_value` token to `_to` from `msg.sender`
/// @param _to The address of the recipient
/// @param _value The amount of token to be transferred
/// @return Whether the transfer was successful or not
function transfer(address _to, uint256 _value) public returns (bool success);

/// @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from`
/// @param _from The address of the sender
/// @param _to The address of the recipient
/// @param _value The amount of token to be transferred
/// @return Whether the transfer was successful or not
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success);

/// @notice `msg.sender` approves `_spender` to spend `_value` tokens
/// @param _spender The address of the account able to transfer the tokens
/// @param _value The amount of tokens to be approved for transfer
/// @return Whether the approval was successful or not
function approve(address _spender, uint256 _value) public returns (bool success);

/// @param _owner The address of the account owning tokens
/// @param _spender The address of the account able to transfer the tokens
/// @return Amount of remaining tokens allowed to spent
function allowance(address _owner, address _spender) public view returns (uint256 remaining);

// solhint-disable-next-line no-simple-event-func-name
event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}
  • 着重需要看的是EIP20的代码实现,我通过一些注释来解释我对代码的理解,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import "./EIP20Interface.sol";


contract EIP20 is EIP20Interface {

uint256 constant private MAX_UINT256 = 2**256 - 1;
mapping (address => uint256) public balances;
mapping (address => mapping (address => uint256)) public allowed;
/*
NOTE:
The following variables are OPTIONAL vanities. One does not have to include them.
They allow one to customise the token contract & in no way influences the core functionality.
Some wallets/interfaces might not even bother to look at this information.
可选的三个变量信息,分别是Token名称,支持小数点经度,通用的都是18位,Token的简称
*/
string public name;
uint8 public decimals;
string public symbol;

constructor(
uint256 _initialAmount,
string _tokenName,
uint8 _decimalUnits,
string _tokenSymbols
) public {
//发行总量,要根据小数点精确度来计算,比如发行100个,精确度为2,那么就是10 * 10的2次方
totalSupply = _initialAmount * 10 ** uint256(_decimalUnits); // Update total supply
//合约创建者默认拥有所有的token
balances[msg.sender] = totalSupply; // Give the creator all initial tokens
name = _tokenName; // Set the name for display purposes
decimals = _decimalUnits; // Amount of decimals for display purposes
symbol = _tokenSymbols; // Set the symbol for display purposes
}

function transfer(address _to, uint256 _value) public returns (bool success) {
require(balances[msg.sender] >= _value);
balances[msg.sender] -= _value;
balances[_to] += _value;
//发送Transfer消息事件
emit Transfer(msg.sender, _to, _value); //solhint-disable-line indent, no-unused-vars
return true;
}

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
uint256 allowance = allowed[_from][msg.sender];
require(balances[_from] >= _value && allowance >= _value);
balances[_to] += _value;
balances[_from] -= _value;
if (allowance < MAX_UINT256) {
allowed[_from][msg.sender] -= _value;
}
emit Transfer(_from, _to, _value); //solhint-disable-line indent, no-unused-vars
return true;
}

function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}

function approve(address _spender, uint256 _value) public returns (bool success) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value); //solhint-disable-line indent, no-unused-vars
return true;
}

function allowance(address _owner, address _spender) public view returns (uint256 remaining) {
return allowed[_owner][_spender];
}
}
  • 而Token管理类的源码比较简单,通过对EIP20的进一步封装,实现了创建Token的接口,并且对EIP20的合约代码做了保存,可以对一个合约是不是EIP20标准做检测。顺便说一句,这个管理类的几个方法对gas的消耗那不是一般的高啊。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import "./EIP20.sol";

pragma solidity ^0.4.21;


contract EIP20Factory {

mapping(address => address[]) public created; //已经创建的token信息
mapping(address => bool) public isEIP20; //验证一个合约是不是EIP20标准,并不做代码检查
bytes public EIP20ByteCode; // EIP20标准的合约代码,用来作对比验证的

constructor() public {
//upon creation of the factory, deploy a EIP20 (parameters are meaningless) and store the bytecode provably.
//构造方法,创建一个token,并且存储下来合约的代码
address verifiedToken = createEIP20(100000000, "TedToken", 18, "TEC");
EIP20ByteCode = codeAt(verifiedToken);
}

//verifies if a contract that has been deployed is a Human Standard Token.
//NOTE: This is a very expensive function, and should only be used in an eth_call. ~800k gas
//验证一个已经部署的合约是不是正常标准的token,
//提示,这个方法相当的消耗gas,至少得需要800k的gas
function verifyEIP20(address _tokenContract) public view returns (bool) {
bytes memory fetchedTokenByteCode = codeAt(_tokenContract);

if (fetchedTokenByteCode.length != EIP20ByteCode.length) {
return false; //clear mismatch
}

//starting iterating through it if lengths match
for (uint i = 0; i < fetchedTokenByteCode.length; i++) {
if (fetchedTokenByteCode[i] != EIP20ByteCode[i]) {
return false;
}
}
return true;
}

//创建EIP20标准的Token
function createEIP20(uint256 _initialAmount, string _name, uint8 _decimals, string _symbol)
public
returns (address) {

EIP20 newToken = (new EIP20(_initialAmount, _name, _decimals, _symbol));
created[msg.sender].push(address(newToken));
isEIP20[address(newToken)] = true;
//the factory will own the created tokens. You must transfer them.
//重要提示:如果通过工具类创建的token,默认所有的Token拥有者是工具类合约,所以一定要转出来
newToken.transfer(msg.sender, (_initialAmount - 100) * 10 ** uint256(_decimals));
return address(newToken);
}

//for now, keeping this internal. Ideally there should also be a live version of this that
// any contract can use, lib-style.
//retrieves the bytecode at a specific address.
function codeAt(address _addr) internal view returns (bytes outputCode) {
assembly { // solhint-disable-line no-inline-assembly
// retrieve the size of the code, this needs assembly
let size := extcodesize(_addr)
// allocate output byte array - this could also be done without assembly
// by using outputCode = new bytes(size)
outputCode := mload(0x40)
// new "memory end" including padding
mstore(0x40, add(outputCode, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// store length in memory
mstore(outputCode, size)
// actually retrieve the code, this needs assembly
extcodecopy(_addr, add(outputCode, 0x20), 0, size)
}
}
}

0x2 Token示例源码二

openzeppelin-solidity/contracts/token/ERC20 at master · OpenZeppelin/openzeppelin-solidity · GitHub

0x3 关于approve的安全代码示例

minime/MiniMeToken.sol at master · Giveth/minime · GitHub

0%