ERC20:从入门到飞起,妈妈再也不用担心我不会写Token合约了
点击跳转至本项目 Github 仓库地址
由于篇幅有限,本博客将围绕ERC20核心展开介绍,文章内容尽量做到通俗易懂,但其中不可避免地可能涉及一些新手不友好的概念,您可以查阅相关博客做进一步了解,本系列博客也会不断扩充、提升及优化,尽量做到不留死角,人人都能上手Solidity标准开发。
0. ERC 是什么鬼?
ERC 全称 Ethereum Request For Comment (以太坊意见征求稿), 是以太坊上应用级的开发标准和协议(application-level standards and conventions),为以太坊开发人员提供了实施标准,开发人员可以使用这些标准来构建智能合约。
ERC 的雏形是开发人员提交的EIP(Ethereum Improvement Proposals),即新的ERC标准提案,一旦其得到以太坊委员会的批准并最终确定,新的ERC便由此诞生。
1. 初识 ERC 20
ERC 20 是同质资产 FT 的 API 标准,提供了资产转移、余额查询、授权第三方转移资产等资产花费的实用接口,开发人员可进一步扩充资产发行与回收、提升或降低第三方转移资产的额度等实用功能。本文将逐一介绍 ERC20 标准核心接口的含义、扩充建议及开发示例。
2. ERC20 标准庐山真面
2.1 Token 身份证
您可以选用 name 、symbol 和 decimals 三个变量存储您 Token 的基本信息,如 Token 名称、 Token 标识符和Token 小数点右边的精度 。如果您将其定义为 public 变量,合约成功编译并部署后会自动生成相应的 public view 函数,所有人将能够免费调用它们并在返回值内得到相应变量当前存储的信息。
您可以选择在定义时就为其赋初值,或者在合约部署时选用构造函数为它们赋值,下图是在 Remix 中选用构造函数为其赋值的一个示例,您可以在下文复制相应的示例代码片段。
示例代码:
import "SafeMath.sol";
pragma solidity ^0.8.6;
contract ERC20Example {
string public name;
string public symbol;
uint8 public decimals;
constructor() {
name = "Example Token";
symbol = "ET";
decimals = 8;
}
}
2.2 两个了不起的映射朋友 您可以使用映射 balances 记录当前每个用户地址(用户钱包、用户账户)中该 Token 的余额,用户地址将作为 balances 映射的键(即 key),余额将作为balances 映射的值(即 value);
您可以使用映射 allowed 记录用户授权第三方转移其拥有 Token 数量的上限,用户地址将作为 allowed 映射的键(即 key),allowed 映射的值(即 value)是一个映射,第三方地址将作为该映射的键,第三方可转移的 Token 数量上限将作为该映射的值。
下图是在 Remix 中的示例代码,您可以在下文复制相应的示例代码片段,这两个映射的使用方式将后续文章内容中逐一介绍。
示例代码:
mapping (address => uint256) public balances;
mapping (address => mapping (address => uint256)) public allowed;
2.3 发行总量三件套 2.3.1 发行总量 首先,请为您的 Token 合约添加一个记录发行总量的 private 变量 _totalSupply,接着,您可以通过 public 函数 totalSupply 返回该变量的值,如果您想减少代码编写量,也可以直接定义一个 public 变量 totalSupply,它和上文提到的可选变量一样,会在合约成功编译并部署后自动生成相应的 public view 函数,来向调用者展示当前该变量的值。
如果您希望初始发行量为0 ,那么您可以不为它赋初值,因为它的默认初值就是0;如果您希望指定初始发行量 “amount eg 50000” ,您可以在定义该变量时给它赋值,或者在构造函数中为它赋值 ,如果您选择指定初始发行量为amount,记得在构造函数中通过 balances 映射将初始发行的 token 数量“转移”到指定地址 :balances[msg.sender] = amount ,否则这部分发行的 Token 将无人可用,也不会为市场提供任何的流动性。
下图是在 Remix 中的示例代码,默认合约部署时的初始发行量为50000,初始发行的 Token 将存入合约部署者的余额,您可以在下文复制相应的示例代码片段。
示例代码:
contract ERC20Example {
uint256 private _totalSupply;
constructor() {
_totalSupply = 50000;
balances[msg.sender] = _totalSupply;
}
function totalSupply() public view returns (uint256) {
return _totalSupply;
}
function balanceOf(address owner) public view returns (uint256) {
return balances[owner];
}
}
2.3.2 发行与回收 最后,您可以通过提供 mint 函数和 brun 函数来动态调节发行总量。建议您为这两个函数添加 onlyOwner 修饰符以确保仅该 Token 合约的主人可以调用,您也可以增加其他地址作为管理员共同管理 Token 的发行与回收。
更多关联教程: Ownable 教程:从入门到不离不弃 (onlyOwner是什么鬼?)【coming soon】 ERC20 共同发行管理员教程:从此不再孤单一人 【coming soon】
mint 函数用于铸造并向市场发行更多的 Token ,burn 函数用于回收并销毁已经发行到市场中的 Token 。实现 mint 和 burn 函数前,让我们首先引入 uint256 变量的 SafeMath 库,并声明为 uint256 变量使用 SafeMath 库中的安全计算方法;其次,让我们增加一个 public 变量 owner ,并在构造函数中为其赋值为部署该合约的用户地址,并编写修饰符 onlyOwner 限制仅 owner 地址可以调用所有带有该修饰符的函数。
下图是在 Remix 中的示例代码,您可以在下文复制相应的示例代码片段。您需要将 uint256 变量的 SafeMath 库放入当前合约同一目录下,您可以在这里获得 SafeMath 库的示例代码。
示例代码:
import "SafeMath.sol";
pragma solidity >=0.7.0 <0.9.0;
contract ERC20Example {
using SafeMath for uint256;
address public tokenOwner;
constructor() {
tokenOwner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == tokenOwner, "Caller is not owner");
_;
}
}
好啦,现在让我们一起看一下 mint 和 burn 函数在 Remix 中的示例代码,您可以在下文复制相应的示例代码片段。mint 和 burn 函数都被修饰符 onlyOwner 限定为仅 owner 地址可调用。
mint 函数体第一行(下图第70行)首先要求发行 Token 转移到的地址是正常地址(非0地址),接着通过 SafeMath 中的 add 函数增加该 Token 的发行总额,随后增加接收该发行 Token 地址的余额,最后触发一个 Transfer 转账事件表明该发行过程成功结束;
burn 函数体第一行(下图第84行)首先要求从正常地址(非0地址)回收 Token ,随后判断该地址余额是否充足,接着通过 SafeMath 中的 sub 函数减少该 Token 的发行总额,随后减少该回收地址的余额,最后触发一个 Transfer 转账事件表明该发行过程成功结束。
Solidity中的事件可用于标识一件事情成功发生,DAPP等后续智能合约之上的应用可监听相应的事件开展进一步操作。
更多关联教程: 以太坊0地址科普 【coming soon】 mint 和 burn 的更多玩法 【coming soon】 为什么我们需要 SafeMath 【coming soon】
示例代码:
function mint(address account, uint256 amount) public onlyOwner {
require(account != address(0));
_totalSupply = _totalSupply.add(amount);
balances[account] = balances[account].add(amount);
emit Transfer(address(0), account, amount);
}
function burn(address account, uint256 amount) public onlyOwner {
require(account != address(0));
require(amount <= balances[account]);
_totalSupply = _totalSupply.sub(amount);
balances[account] = balances[account].sub(amount);
emit Transfer(account, address(0), amount);
}
2.4 转移一笔 Token 您可以提供一个 public 函数 transfer 允许用户调用以进行 Token 转移,函数体第一行(下图第174行)首先判断调用者地址余额是否充足,接着要求必须转入正常地址(非0地址),接着分别通过 SafeMath 中的 sub 函数和 add 函数修改调用者和接收者的余额,然后触发一个 Transfer 转账事件表明该转账过程成功结束,并返回 true。下图是在 Remix 中的示例代码,您可以在下文复制相应的示例代码片段。
示例代码:
function transfer(address to, uint256 value) public returns (bool) {
require(value <= balances[msg.sender]);
require(to != address(0));
balances[msg.sender] = balances[msg.sender].sub(value);
balances[to] = balances[to].add(value);
emit Transfer(msg.sender, to, value);
return true;
}
2.5 查看指定地址的 Token 余额 您可以提供一个 public 函数 balanceOf 允许用户调用以查看任何地址的余额,如下图108-110行所示,该函数返回指定地址在 balances 映射中对应的值。下图是在 Remix 中的示例代码,您可以在下文复制相应的示例代码片段。
示例代码:
contract ERC20Example {
mapping (address => uint256) public balances;
function balanceOf(address tokenOwner) public view returns (uint256) {
return balances[tokenOwner];
}
2.6 授权其他地址转移 Token 您可以提供一个 public 函数 approve 允许用户调用以授权其他地址转移自己的可用余额,并配套提供一个 public 函数 allowance 允许用户调用以查看任何地址当前授权任意地址转移其余额的上限。
下图是在 Remix 中的示例代码,您可以在下文复制相应的示例代码片段。
如下图132-136行所示,approve函数首先要求允许转移调用者余额的地址是正常地址(非0地址),接着修改allowed映射中相应的键值,然后触发事件 Approval 表示授权过程成功完成并返回true。
如下图121行所示,该函数返回指定地址 owner 授权给另一地址 spender 的可转移余额上限。
示例代码:
function allowance(address owner,address spender) public view returns (uint256) {
return allowed[owner][spender];
}
function approve(address spender, uint256 value) public returns (bool) {
require(spender != address(0));
allowed[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
注意,每次调用 approve 函数都会重写授权消费的上限金额,您可以增加两个借口方便用户对授权金额进行增减,而不仅限于重写授权金额。下图是在 Remix 中的示例代码,您可以在下文复制相应的示例代码片段。
如下图145-150行所示,increaseAllowance允许调用者增加授权者的消费额度,并触发一个 Approval 事件; 如下图159-164行所示,decreaseAllowance允许调用者减少授权者的消费额度,并触发一个 Approval 事件。
function increaseAllowance(address spender,uint256 addedValue) public returns (bool) {
require(spender != address(0));
allowed[msg.sender][spender] = (allowed[msg.sender][spender].add(addedValue));
emit Approval(msg.sender, spender, allowed[msg.sender][spender]);
return true;
}
function decreaseAllowance(address spender,uint256 subtractedValue) public returns (bool) {
require(spender != address(0));
allowed[msg.sender][spender] = (allowed[msg.sender][spender].sub(subtractedValue));
emit Approval(msg.sender, spender, allowed[msg.sender][spender]);
return true;
}
2.7 大胆花费别人的 Token 您可以提供一个 public 函数 transferFrom 允许用户调用以花费别人授权给自己的额度。 下图是在 Remix 中的示例代码,您可以在下文复制相应的示例代码片段。如下图192-194行所示,transferFrom函数首确保转出 Token 的账户地址余额充足,接着判断调用者是否拥有足够的授权额度,然后要求接收者地址是正常地址(非0地址),随后转账流程开始,如下图196-200行所示,分别通过 SafeMath 中的 sub 函数和 add 函数修改转出者和接收者的余额,然后对调用者的授权额度进行修改,接着触发一个 Transfer 转账事件表明该转账过程成功结束,并返回 true。
示例代码:
function transferFrom(address from,address to,uint256 value) public returns (bool) {
require(value <= balances[from]);
require(value <= allowed[from][msg.sender]);
require(to != address(0));
balances[from] = balances[from].sub(value);
balances[to] = balances[to].add(value);
allowed[from][msg.sender] = allowed[from][msg.sender].sub(value);
emit Transfer(from, to, value);
return true;
}
2.8 最后提一嘴之合约事件 ERC20 给出事件 Transfer 和 Approve 分别表示 Token 的转移和授权操作成功发生(打包上链),事件名称以大写字母开头,无函数体实现。
|