ERC20 规定了一个同质化代币(Fungible Token)需要实现的基本接口。至于什么是同质化代币,可参考上篇文章: NFT 这么火,你还不懂 ERC721 吗?
1. 接口定义
接口定义如下,定义了 6 个接口、 2个事件。
接口主要涉及诸如:代币发行量、查询账户余额、转账、授权代币使用权、查看授权代币数量、授权转账。
事件主要描述:转移事件、授权事件。
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* 该代币总发行量
*/
function totalSupply() external view returns (uint256);
/**
* 返回一个账户余额
*/
function balanceOf(address account) external view returns (uint256);
/**
* 转账 (从调用者的代币中转移给 receipient 账号)
* 会执行 Transfer 事件
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* 返回 spender 账户被允许使用 owner 账户的 代币数(默认是0)
* 意思是:owner 可以授权自己的账户给他人使用自己的代币。
* 该数值会在 approve 函数 or transferFrom 函数被调用时发生变化
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* 调用者授予 spender 指定数量(amount)的代币的使用权;返回bool(要么成功要么失败)
* 会触发 Approval 事件
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* 授权转账;从 sender 发送 amount 数量的代币给到 recipient 。
* 会触发 Transfer 事件
* Emits a {Transfer} event.
*/
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
/**
* 转移事件
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* 授权事件
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
2. 源码分析
OZ 对 ERC20 规范进行了实现,接下来我们来看看~
Tip:源码中以 _ 开头的函数是内部函数,主要用以内部调用。
2.1 合约继承
contract ERC20 is Context, IERC20, IERC20Metadata {
}
可以看到 ERC20 合约继承了 Context、IERC20、IERC20Metadata,我们来看看 Context 和 IERC20Metadata 的定义。
2.1.1 Context
上下文处理,只是对 msg 对象做了一些包装。
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
pragma solidity ^0.8.0;
/**
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
}
2.1.2 IERC20Metadata
定义了代币的元数据(name、symbol),以及代币的精度。
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol)
pragma solidity ^0.8.0;
import "../IERC20.sol";
/**
* @dev Interface for the optional metadata functions from the ERC20 standard.
*
* _Available since v4.1._
*/
interface IERC20Metadata is IERC20 {
/**
* @dev Returns the name of the token.
*/
function name() external view returns (string memory);
/**
* @dev Returns the symbol of the token.
*/
function symbol() external view returns (string memory);
/**
* @dev Returns the decimals places of the token.
*/
function decimals() external view returns (uint8);
}
2.2 构造器&成员变量
// 总发行量
uint256 private _totalSupply;
// 代币 name 和 symbol标识符
string private _name;
string private _symbol;
// 构造器
constructor (string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
// 账户 --> 代币数的映射
mapping (address => uint256) private _balances;
//账号A --> (账号B --> 数量)的映射; 即 账号A对账号B授权指定数量的代币的决定权;即账号B拥有账号A的指定数量代币的行使权。
mapping (address => mapping (address => uint256)) private _allowances;
2.3 查询(Query)
查询包括:代币名称查询、代币标志符查询、代币精度查询、代币总发行量查询、用户资产查询、授权额度查询
/**
* @dev 返回代币名称
*/
function name() public view virtual override returns (string memory) {
return _name;
}
/**
* @dev 返回代币标志符
*/
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
/**
* @dev 返回代币精度(一份代币可以切割多少份);例如 eth 也是 18
*/
function decimals() public view virtual override returns (uint8) {
return 18;
}
/**
* @dev 代币总发行量
*/
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
/**
* @dev 查询账户余额
*/
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
/**
* @dev 获取 owner 对 spender 的授权额度
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
2.5 授权转账(transferFrom)
调用者拥有 Sender 账号的一定数量的代币行使权。可将 Sender 账号的 amount 数量代币转账给 Recipient 账号。
/**
* 授权转账
* 转账触发 Transfer 事件
* 触发 Approval 事件,更新 allowance 数量
* 要求: 1. Sender、Recipient 不能是零地址 2. Sender 拥有不少于 amount 数量的代币 3.调用者 msg.Sender 拥有 Sender 不少于 amount 数量的代币的行使权。
*/
function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
// 调用转账函数 (疑问:为啥不先进行下面的判断后再进行转账?)
_transfer(sender, recipient, amount);
// 获取 Sender 账号对 msg.Sender 账号的授权代币数量
uint256 currentAllowance = _allowances[sender][_msgSender()];
// 授权数 >= amount 方可进行转账
require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
// 执行 approve 授权函数,更新授权代币数量(会触发Approval事件)
_approve(sender, _msgSender(), currentAllowance - amount);
return true;
}
2.6 授权额度 (approve)
该函数支持将调用者的 amount 数量的代币授予 spender 行使。
注意:该函数的调用效果并非叠加;例如第一次调用授予 10 个,第二次调用授予 20 个;按照常理则理论上是授予了 30 个代币的行使权。但是这里是覆盖的效果,取最后一次授予的数量为准,也即是最终只授予了 20 个代币的行使权。
/**
* @dev public函数:授权额度函数
* 调用者将
*/
function approve(address spender, uint256 amount) public virtual override returns (bool) {
// 调用者授予 spender 指定 amount 数量的代币行使权
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
/**
* 内部函数:真正执行授权额度,将 owner 的 amount 数量的代币授予 spender 行使权。
* 事件会触发 Approval 事件
*/
function _approve(address owner, address spender, uint256 amount) internal virtual {
// owner 和 spender 不能是零地址
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
// 授权额度映射,更改数值为 amount (注意:是覆盖而非叠加效果)
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
从上面的讲解可以看到,该函数的效果是 覆盖 而非 叠加 ,那如果希望是在原有的授权数量进行叠加的效果,又该如何呢?
其实源码中也提供了相关的函数:
- increaseAllowance:在原有的授权额度之上增加额度。
- decreaseAllowance:在原有的授权额度之上减少额度。
/**
* @dev 原子的增加授权额度
* 调用者叠加 addedValue 数量的代币给 spender 使用。
*/
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
_approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue);
return true;
}
/**
* @dev 原子的减少授权额度
* 调用者减少 subValue 数量的代币给 spender 使用。
*/
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
// 前置判断,不能减为负数
uint256 currentAllowance = _allowances[_msgSender()][spender];
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
_approve(_msgSender(), spender, currentAllowance - subtractedValue);
return true;
}
2.4 转账(transfer)
转账是ERC20规范定义的核心功能。
/**
* public函数:对外提供
* 调用者转amount数量的代币给 recipient 账号
* 要求:Recipient 不能为零地址(Zero Address)、调用者必须拥有至少 amount 数量的代币
*/
function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
_transfer(_msgSender(), recipient, amount);
return true;
}
/**
* 真正的转账函数(内部函数),将 sender 用户的 amount 数量代币转账给 recipient 账号
* 要求:
* 1. sender 不能是零地址(zero address)
* 2. receipient 不能是零地址(zero address)
* 3. sender 发送方必须拥有大于等于 amount 数量的代币
*/
function _transfer(address sender, address recipient, uint256 amount) internal virtual {
// sender不能是零地址
require(sender != address(0), "ERC20: transfer from the zero address");
// recipient 不能是零地址
require(recipient != address(0), "ERC20: transfer to the zero address");
// 空函数,说是Hook钩子函数。
_beforeTokenTransfer(sender, recipient, amount);
// sender的代币数量至少大于amount转账数
uint256 senderBalance = _balances[sender];
require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
// 执行账号代币的转移;转账者-=amount 收账者+=amount
_balances[sender] = senderBalance - amount;
_balances[recipient] += amount;
// 触发 转账事件
emit Transfer(sender, recipient, amount);
}
2.7 铸造代币(_mint)
上面介绍了诸如:转账、授权等等函数,但我们似乎忘记了一个核心的问题: 代币呢?钱呢?得有人 “印钱” 才行呀!
_mint 函数即是用来 “印钱” 的,细心的人可能已经发现了:该函数是一个内部函数;这是因为 oz 希望的是:我们开发自己的代币合约,然后调用该 _mint 函数执行铸造。
/**
* 内部方法:铸造代币 (有点类似直接打款到账号 account 上)
* 触发 Transfer 事件
*/
function _mint(address account, uint256 amount) internal virtual {
// 接收代币的账号不能为零地址
require(account != address(0), "ERC20: mint to the zero address");
// hook 函数,可忽略
_beforeTokenTransfer(address(0), account, amount);
// 总发行量累加, 收款账号金额累加
_totalSupply += amount;
_balances[account] += amount;
// 触发 Transfer 事件
emit Transfer(address(0), account, amount);
// hook 函数,可忽略
_afterTokenTransfer(address(0), account, amount);
}
这里可能会存在整型的溢出风险,可以从以下两个方面进行规避:
- 数值操作前后的溢出检查(不符合则不执行)
- 使用 SafeMath 函数进行数值计算 (using SafeMath for uint256 即可)
2.8 销毁代币(_burn)
代币的销毁,也是一个内部函数,由我们自己的代币合约来调用。
/**
* @dev 代币销毁;大体上是将 account 的代币转移到了零地址
*
*/
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
_balances[account] = accountBalance - amount;
_totalSupply -= amount;
emit Transfer(account, address(0), amount);
}
3. 总结
以上这些就是 ERC20 规范的定义以及 oz 的实现;当我们在编写代币合约的时候,可以集成于 oz 的 ERC20 合约进行重写。
其实在规范和我们自己的业务逻辑的关系上,我是存有疑问:" 在实现了规范接口后,我们可以扩展合约的功能吗?"
答案是:可以的。规范定义的是一套接口,我们在实现了接口之后,是可以扩展定义自己的函数的。例如,我可以额外定义函数A 用以冻结账号…