源码级讲解 ERC20 规范,你确定不看看吗?

1,435 阅读8分钟

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 合约继承了 ContextIERC20IERC20Metadata,我们来看看 ContextIERC20Metadata 的定义。

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 用以冻结账号…