ERC20解读 | 登链社区 | 区块链技术社区 (2023)

参考 OpenZepplin文档以太坊官方开发者文档,结合自己的理解。

博客的 Markdown 编辑器暂不支持 Solidity 语法高亮,为了更好阅读代码,可以去 我的GitHub仓库

什么是ERC20

ERC20(Ethereum Request for Comments 20)一种代币标准。EIP-20 中提出。

ERC20 代币合约跟踪同质化(可替代)代币:任何一个代币都完全等同于任何其他代币;没有任何代币具有与之相关的特殊权利或行为。这使得 ERC20 代币可用于交换货币、投票权、质押等媒介。

为什么要遵守ERC20

EIP-20 中的动机:

(Video) 【登链公共课】跨链主流技术机制浅析

允许以太坊上的任何代币被其他应用程序(从钱包到去中心化交易所)重新使用的标准接口。

以太坊上的所有应用都默认支持 ERC20 ,如果你想自己发币,那么你的代码必须遵循 ERC20 标准,这样钱包(如MetaMask)等应用才能将你的币显示出来。

代码实现

需要实现以下函数和事件:

function name() public view returns (string) function symbol() public view returns (string) function decimals() public view returns (uint8) function totalSupply() public view returns (uint256) function balanceOf(address _owner) public view returns (uint256 balance) function transfer(address _to, uint256 _value) public returns (bool success) function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) function approve(address _spender, uint256 _value) public returns (bool success) function allowance(address _owner, address _spender) public view returns (uint256 remaining) ​ event Transfer(address indexed _from, address indexed _to, uint256 _value) event Approval(address indexed _owner, address indexed _spender, uint256 _value)

使用 OpenZeppllin 提供的库能够轻松快速地构建 ERC20 Token 。

快速构建

这是一个 GLD token 。

(Video) 【登链公开课】ERC2612 (ERC20-Permit) 实现链下无 gas 授权

// contracts/GLDToken.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; ​ import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; ​ contract GLDToken is ERC20 { constructor(uint256 initialSupply) ERC20("Gold", "GLD") { _mint(msg.sender, initialSupply); } }

通常,我们定义代币的发行量和代币名称及符号。

IERC20

先来看下 ERC20 的接口(IERC20),这方便我们在开发中直接定义 ERC20 代币。

同样地,OpenZepplin 为我们提供了相应的库,方便开发者导入即用。

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

EIP 中定义的 ERC20 标准接口:

pragma solidity ^0.8.0; ​ interface IERC20 { event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); function transferFrom( address from, address to, uint256 amount ) external returns (bool); }

逐一分析

函数:

(Video) 【登链公开课】FVM合约开发入门

  • totalSupply() :返回总共的代币数量。
  • balanceOf(address account) :返回 account 地址拥有的代币数量。
  • transfer(address to, uint256 amount) :将 amount 数量的代币发送给 to 地址,返回布尔值告知是否执行成功。触发 Transfer 事件。
  • allowance(address owner, address spender) :返回授权花费者 spender 通过 transferFrom 代表所有者花费的剩余代币数量。默认情况下为零。当 approvetransferFrom 被调用时,值将改变。
  • approve(address spender, uint256 amount) :授权 spender 可以花费 amount 数量的代币,返回布尔值告知是否执行成功。触发 Approval 事件。
  • transferFrom(address from, address to, uint256 amount) :将 amount 数量的代币从 from 地址发送到 to 地址,返回布尔值告知是否执行成功。触发 Transfer 事件。

事件(定义中的 indexed 便于查找过滤):

  • Transfer(address from, address to, uint256 value) :当代币被一个地址转移到另一个地址时触发。注意:转移的值可能是 0 。
  • Approval(address owner, address spender, uint256 value) :当代币所有者授权别人使用代币时触发,即调用 approve 方法。

元数据

一般除了上述必须实现的函数外,还有一些别的方法:

  • name() :返回代币名称
  • symbol() :返回代币符号
  • decimals() :返回代币小数点后位数

ERC20

来看下 ERC20 代币具体是怎么写的。

同样,OpenZepplin 提供了现成的合约代码:

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

这里贴一个GitHub源码链接 OpenZepplin ERC20

(Video) 【登链公开课】轻客户端原理与应用

函数概览

constructor(name_, symbol_) name() symbol() decimals() totalSupply() balanceOf(account) transfer(to, amount) allowance(owner, spender) approve(spender, amount) transferFrom(from, to, amount) increaseAllowance(spender, addedValue) decreaseAllowance(spender, subtractedValue) _transfer(from, to, amount) _mint(account, amount) _burn(account, amount) _approve(owner, spender, amount) _spendAllowance(owner, spender, amount) _beforeTokenTransfer(from, to, amount) _afterTokenTransfer(from, to, amount)

事件(同 IERC20)

Transfer(from, to, value) Approval(owner, spender, value)

逐一分析

  • constructor(string name, string symbol) :设定代币的名称和符号。decimals 默认是 18 ,要修改成不同的值你应该重载它。这两个值是不变的,只在构造时赋值一次。
  • name() :返回代币的名称。
  • symbol() :返回代币的符号,通常是名称的缩写。
  • decimals() :返回小数点后位数,通常是 18 ,模仿 Ether 和 wei 。要更改就重写它。

totalSupply()、balanceOf(address account)、transfer(address to, uint256 amount)、 allowance(address owner, address spender)、approve(address spender, uint256 amount)、transferFrom(address from, address to, uint256 amount) 都参考 IERC20 。

  • increaseAllowance(address spender, uint256 addedValue) :以原子的方式增加 spender 额度。返回布尔值告知是否执行成功,触发 Approval 事件。
  • _transfer(address from, address to, uint256 amount) :转账。这个内部函数相当于 transfer ,可以用于例如实施自动代币费用,削减机制等。触发 Transfer 事件。
  • _mint(address account, uint256 amount) :铸造 amount 数量的代币给 account 地址,增加总发行量。触发 Transfer 事件,其中参数 from 是零地址。
  • _burn(address account, uint256 amount) :从 account 地址中烧毁 amount 数量的代币,减少总发行量。触发 Transfer 事件,其中参数 to 是零地址。
  • _approve(address owner, uint256 spender, uint256 amount) :设定允许 spender 花费 owner 的代币数量。这个内部函数相当于 approve ,可以用于例如为某些子系统设置自动限额等。
  • spendAllowance(address owner, address spender, uint256 amount) :花费 amount 数量的 owner 授权 spender 的代币。在无限 allowance 的情况下不更新 allowance 金额。如果没有足够的余量,则恢复。可能触发 Approval 事件。
  • _beforeTokenTransfer(address from, address to, uint256 amount) :在任何代币转账前的 Hook 。它包括铸币和烧毁。调用条件:
    • fromto 都不是零地址时,from 手里 amount 数量的代币将发送给 to
    • from 是零地址时,将给 to 铸造 amount 数量的代币。
    • to 是零地址时,from 手里 amount 数量的代币将被烧毁。
    • fromto 不能同时为零地址。
  • _afterTokenTransfer(address from, address to, uint256 amount) :在任何代币转账后的 Hook 。它包括铸币和烧毁。调用条件:
    • fromto 都不是零地址时,from 手里 amount 数量的代币将发送给 to
    • from 是零地址时,将给 to 铸造 amount 数量的代币。
    • to 是零地址时,from 手里 amount 数量的代币将被烧毁。
    • fromto 不能同时为零地址。

小结

ERC20 代码中的 _transfer_mint_burn_approve_spendAllowance_beforeTokenTransfer_afterTokenTransfer 都是 internal 函数(其余为 public ),也就是说它们只能被派生合约调用。

从零开始,自己动手

1.编写IERC20

IERC20.sol

// SPDX-License-Identifier: MIT ​ pragma solidity ^0.8.0; ​ interface IERC20 { /// @dev 总发行量 function totoalSupply() external view returns (uint256); /// @dev 查看地址余额 function balanceOf(address account) external view returns (uint256); /// @dev 单地址转账 function transfer(address account, uint256 amount) external returns (bool); /// @dev 查看被授权人代表所有者花费的代币余额 function allowance(address owner, address spender) external view returns (uint256); /// @dev 授权别人花费你拥有的代币 function approve(address spender, uint256 amount) external returns (bool); /// @dev 双地址转账 function transferFrom( address from, address to, uint256 amount ) external returns (bool); ​ /// @dev 发生代币转移时触发 event Transfer(address indexed from, address indexed to, uint256 value); /// @dev 授权时触发 event Approval(address indexed owner, address indexed spender, uint256 value); }

2.加上Metadata

IERC20Metadata.sol

(Video) 【登链公开课】The Graph 如何提高 Dapp 开发效率

// SPDX-License-Identifier: MIT ​ pragma solidity ^0.8.0; ​ import "IERC20.sol"; ​ interface IERC20Metadata is IERC20 { /// @dev 代币名称 function name() external view returns (string memory); /// @dev 代币符号 function symbol() external view returns (string memory); /// @dev 小数点后位数 function decimals() external view returns (uint8); }

3.编写ERC20

ERC20.sol

// SPDX-License-Identifier: MIT ​ pragma solidity ^0.8.0; ​ import "./IERC20.sol"; import "./IERC20Metadata.sol"; ​ contract ERC20 is IERC20, IERC30Metadata { // 地址余额 mapping(address => uint256) private _balances; // 授权地址余额 mapping(address => mapping(address => uint256)) private _allowances; ​ uint256 private _totalSupply; ​ string private _name; string private _symbol; ​ /// @dev 设定代币名称符号 constructor(string memory name_, string memory symbol_) { _name = name_; _symbol = symbol_; } ​ function name() public view virtual override returns (string memory) { return _name; } ​ function symbol() public view virtual override returns (string memory) { return _symbol; } ​ /// @dev 小数点位数一般为 18 function decimals() public view virtual override returns (uint8) { return 18; } ​ function totalSupply() public view virtual override returns (uint256) { return _totalSupply; } ​ function balanceOf(address account) public view virtual override returns (uint256) { return _balances[account]; } ​ function transfer(address to, uint256 amount) public virtual override returns (bool) { address owner = msg.sender; _transfer(owner, to, amount); return true; } ​ function allowance(address owner, address spender) public view virtual override returns (uint256) { return _allowances[owner][spender]; } ​ function approve(address spender, uint256 amount) public virtual override returns (bool) { address owner = msg.sender; _approve(owner, spender, amount); return true; } ​ function transferFrom( address from, address to, uint256 amount ) public virtual override returns (bool) { address spender = msg.sender; _spendAllowance(from, spender, amount); _transfer(from, to, amount); return true; } ​ function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { address owner = msg.sender; _approve(owner, spender, _allowances[owner][spender] + addedValue); return true; } ​ function decreaseAllowance(address spender, uint256 substractedValue) public virtual returns (bool) { address owner = msg.sender; uint256 currentAllowance = _allowances[owner][spender]; require(currentAllowance >= substractedValue, "ERC20: decreased allowance below zero"); unchecked { _approval(owner, spender, currentAllowance - substractedValue); } return true; } ​ function _transfer( address from, address to, uint256 amount ) internal virtual { require(from != address(0), "ERC20: transfer from the zero address"); require(to != address(0), "ERC20: transfer to the zero address"); ​ _beforeTokenTransfer(from, to, amount); ​ uint256 fromBalance = _balances[from]; require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); unchecked { _balances[from] = fromBalance - amount; } _balances[to] += amount; ​ emit Transfer(from, to, amount); ​ _afterTokenTransfer(from, to, amount); } ​ function _mint(address account, uint256 amount) internal virtual { require(account != address(0), "ERC20: mint to the zero address"); ​ _beforeTokenTransfer(address(0), account, amount); ​ _totalSupply += amount; _balances[account] += amount; emit Transfer(address(0), account, amount); ​ _afterTokenTransfer(address(0), account, amount); } ​ 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"); unchecked { _balances[account] = accountBalance - amount; } _totalSupply -= amount; ​ emit Transfer(account, address(0), amount); ​ _afterTokenTransfer(account, address(0), amount); } ​ function _approve( address owner, address spender, uint256 amount ) internal virtual { require(owner != address(0), "ERC20: approve from the zero address"); require(spender != address(0), "ERC20: approve to the zero address"); ​ _allowances[owner][spender]; emit Approval(owner, spender, amount); } ​ function _spendAllowance( address owner, address spender, uint256 amount ) internal virtual { uint256 currentAllowance = allowance(owner, spender); if (currentAllowance != type(uint256).max) { require(currentAllowance >= amount, "ERC20: insufficient allowance"); unchecked { _approve(owner, spender, currentAllowance - amount); } } } ​ function _beforeTokenTransfer( address from, address to, uint256 amount ) internal virtual {} ​ function _afterTokenTransfer( address from, address to, uint256 amount ) internal virtual {} }

总结

ERC20 其实就是一种最常见的代币标准,它明确了同质化代币的经典功能并规范了开发者编写 token 时的代码,从而方便各种应用适配。

Videos

1. 【登链公开课】账户抽象与智能钱包
(登链社区)
2. 【登链公开课】数据分析-追踪加密货币的资金流动
(登链社区)
3. 【登链公开课】- 如何实现链上治理
(登链社区)
4. 区块链钱包简介
(登链社区)
5. 【登链公开课】玩转 Nervos EVM 兼容层 dApp 开发
(登链社区)
6. 【登链公开课】构建以太坊的存储二层网络
(登链社区)
Top Articles
Latest Posts
Article information

Author: Dean Jakubowski Ret

Last Updated: 01/28/2023

Views: 6641

Rating: 5 / 5 (50 voted)

Reviews: 81% of readers found this page helpful

Author information

Name: Dean Jakubowski Ret

Birthday: 1996-05-10

Address: Apt. 425 4346 Santiago Islands, Shariside, AK 38830-1874

Phone: +96313309894162

Job: Legacy Sales Designer

Hobby: Baseball, Wood carving, Candle making, Jigsaw puzzles, Lacemaking, Parkour, Drawing

Introduction: My name is Dean Jakubowski Ret, I am a enthusiastic, friendly, homely, handsome, zealous, brainy, elegant person who loves writing and wants to share my knowledge and understanding with you.