Openzeppelin中的ERC20库只提供了mint接口,而具体的发行逻辑需要开发者在其子合约中使用_mint()
自行编写。该库同样遵循了OpenZeppelin的合约设计思路:当函数因产生错误返回false时,直接revert掉。这种设计思路与ERC20的期望标准并不冲突。
[openzeppelin]:v4.8.3,[forge-std]:v1.5.6
Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/token/ERC20/ERC20.sol
Openzeppelin中的ERC20库只提供了mint接口,而具体的发行逻辑需要开发者在其子合约中使用_mint()
自行编写。该库同样遵循了OpenZeppelin的合约设计思路:当函数因产生错误返回false时,直接revert掉。这种设计思路与ERC20的期望标准并不冲突。
此外,调用transferFrom()
时会抛出Approval
事件,应用可以通过监听该event来重建所有账户的授权额度。其他EIP的实现合约可能不会抛出此类事件,因为规范没有明确要求。
该ERC20库中的decreaseAllowance()
和increaseAllowance()
方法非ERC20标准方法。
继承ERC20合约并暴露_mint()
和_burn()
方法:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol)
ERC20(name, symbol) {}
function mint(address account, uint amount) external {
_mint(account, amount);
}
function burn(address account, uint amount) external {
_burn(account, amount);
}
}
全部foundry测试合约:
// 各账户余额的mapping
// key: 账户地址
// value: 账户余额
mapping(address => uint256) private _balances;
// 各账户授权额度的mapping
// key1: 授权人地址
// key2: 操作人地址(被授权的人地址)
// value: 授权额度
mapping(address => mapping(address => uint256)) private _allowances;
// token的发行总量
uint256 private _totalSupply;
// ERC20 token的名字
string private _name;
// ERC20 token的符号,通常是name的缩写
string private _symbol;
// 初始化函数做了name和symbol的设置
// 该库的默认精度(decimals)是18,如果需要使用别的精度请手动修改decimals()的返回值
constructor(string memory name_, string memory symbol_) {
// 只允许在合约部署时设置name和symbol,后续不允许修改
_name = name_;
_symbol = symbol_;
}
// IERC20Metadata中的方法,返回ERC20的name
function name() public view virtual override returns (string memory) {
return _name;
}
// IERC20Metadata中的方法,返回ERC20的symbol
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
// IERC20Metadata中的方法,返回ERC20的精度。如果精度是3且合约中的余额值为12345,那么其在业务逻辑中表示的余额是12345/(10**3)。
// 这种精度的设计是为了使用整数来表示浮点数
function decimals() public view virtual override returns (uint8) {
return 18;
}
// IERC20中的方法,返回ERC20的总发行量
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
// IERC20中的方法,返回account的余额
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
// IERC20中的方法,返回owner授权给spender的额度
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
foundry代码验证:
contract ERC20Test is Test {
MockERC20 private _testing = new MockERC20("test name", "test symbol");
function test_Getter() external {
assertEq(_testing.name(), "test name");
assertEq(_testing.symbol(), "test symbol");
assertEq(_testing.decimals(), 18);
assertEq(_testing.totalSupply(), 0);
}
}
_mint(address account, uint256 amount)
:为account增发数量为amount的ERC20 token,会同步增加total supply的数量。注: 1. acount地址不可为0地址; 2. 该铸币操作同样会抛出Transfer事件,其中的from地址为0地址;_burn(address account, uint256 amount)
:销毁account名下数量为amount的ERC20 token,会同步减少total supply的数量。注:1. account不可为0地址;2. account名下ERC20 token余额要>=amount; 3. 该销毁操作同样会抛出Transfer事件,其中的to地址为0地址。 function _mint(address account, uint256 amount) internal virtual {
// 检查account地址不为0地址
require(account != address(0), "ERC20: mint to the zero address");
// 执行hook函数_beforeTokenTransfer(),可在该方法中自定义转账前操作逻辑
_beforeTokenTransfer(address(0), account, amount);
// 增加ERC20 token的发行总量,改变量为amount
_totalSupply += amount;
// 关闭solidity 0.8版本的安全数学检查
unchecked {
// 增加account的余额,改变量为amount
// 注:此处加法不会发生溢出,这是因为在前面total supply的自增已经做了溢出检查。如果total supply自增amount不溢出,那么account余额自增amount一定不会溢出
_balances[account] += amount;
}
// 抛出Transfer事件,其中from为0地址
emit Transfer(address(0), account, amount);
// 执行hook函数_afterTokenTransfer(),可在该方法中自定义转账后操作逻辑
_afterTokenTransfer(address(0), account, amount);
}
function _burn(address account, uint256 amount) internal virtual {
// 检查account地址不为0地址
require(account != address(0), "ERC20: burn from the zero address");
// 执行hook函数_beforeTokenTransfer(),可在该方法中自定义转账前操作逻辑
_beforeTokenTransfer(account, address(0), amount);
// 获取account的余额
uint256 accountBalance = _balances[account];
// 要求account的余额>=amount
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
// 关闭solidity 0.8版本的安全数学检查
unchecked {
// 减少account的余额,改变量为amount
_balances[account] = accountBalance - amount;
// 减少total supply数量,改变量为amount。此处不会发生减法溢出,因为前面已经做了accountBalance >= amount的校验,自然totalSupply>=accountBalance>=amount
_totalSupply -= amount;
}
// 抛出Transfer事件,其中to为0地址
emit Transfer(account, address(0), amount);
// 执行hook函数_afterTokenTransfer(),可在该方法中自定义转账后操作逻辑
_afterTokenTransfer(account, address(0), amount);
}
// hook函数_beforeTokenTransfer(),可在该方法中自定义转账前的操作逻辑。本合约中该hook函数为空函数
// 传参条件:
// - 当from和to均非0地址时,表示正常转账;
// - 当from为0地址时,表示为to铸币;
// - 当to为0地址时,表示销毁to的token
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}
// hook函数_afterTokenTransfer(),可在该方法中自定义转账后操作逻辑。本合约中该hook函数为空函数
// 传参条件:
// - 当from和to均非0地址时,表示正常转账;
// - 当from为0地址时,表示为to铸币;
// - 当to为0地址时,表示销毁to的token
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}
foundry代码验证:
contract ERC20Test is Test {
MockERC20 private _testing = new MockERC20("test name", "test symbol");
address private owner = address(1);
event Transfer(address indexed from, address indexed to, uint value);
function test_MintAndBurn() external {
// 1. mint()
assertEq(_testing.balanceOf(owner), 0);
assertEq(_testing.totalSupply(), 0);
vm.expectEmit(true, true, false, true, address(_testing));
emit Transfer(address(0), owner, 1024);
_testing.mint(owner, 1024);
assertEq(_testing.balanceOf(owner), 1024);
assertEq(_testing.totalSupply(), 1024);
// revert if account is 0 address when mint
vm.expectRevert("ERC20: mint to the zero address");
_testing.mint(address(0), 1024);
// 2. burn()
vm.expectEmit(true, true, false, true, address(_testing));
emit Transfer(owner, address(0), 1);
_testing.burn(owner, 1);
assertEq(_testing.balanceOf(owner), 1024 - 1);
assertEq(_testing.totalSupply(), 1024 - 1);
// revert if amount > balance when burn
vm.expectRevert("ERC20: burn amount exceeds balance");
_testing.burn(owner, 1024);
// revert if account is 0 address when burn
vm.expectRevert("ERC20: burn from the zero address");
_testing.burn(address(0), 1);
}
}
approve(address spender, uint256 amount)
:IERC20中的方法,调用者给spender授权数量为amount的额度。注: 1. 当授权额度amount设置为type(uint256).max
时,在spender调用transferFrom()时不会做任何与授权相关的检查和修改。这等同于spender拥有了无限的授权额度; 2. spender不可以为0地址;increaseAllowance(address spender, uint256 addedValue)
:调用者为spender追加授权addedValue数量的授权额度。该方法不是IERC20中的方法且spender不可以为0地址;decreaseAllowance(address spender, uint256 subtractedValue)
:调用者为spender减少subtractedValue数量的授权额度。该方法不是IERC20中的方法。注:1. spender不可以为0地址;2. spender来自调用者的额度必须>=subtractedValue。 function approve(address spender, uint256 amount) public virtual override returns (bool) {
// 获取调用者地址
address owner = _msgSender();
// 调用_approve()方法进行授权的操作
_approve(owner, spender, amount);
// 更新授权成功后返回true
return true;
}
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
// 获取调用者地址(即owner)
address owner = _msgSender();
// 修改owner给spender的授权额度,更新后的额度为当前额度+addedValue
_approve(owner, spender, allowance(owner, spender) + addedValue);
// 更新授权成功后返回true
return true;
}
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
// 获取调用者地址(即owner)
address owner = _msgSender();
// 获取当前spender来自owner的授权额度
uint256 currentAllowance = allowance(owner, spender);
// 要求当前spender来自owner的授权额度>=subtractedValue
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
// 关闭solidity 0.8版本的安全数学检查
unchecked {
// 更新spender来自owner的授权额度,额度为currentAllowance - subtractedValue
_approve(owner, spender, currentAllowance - subtractedValue);
}
// 更新授权成功后返回true
return true;
}
// owner授权给spender数量为amount的额度。可以在该internal函数中根据业务需求增加自动为子系统授权的功能
// 注:
// 1. owner地址不能为0地址;
// 2. spender地址不能为0地址
function _approve(
address owner,
address spender,
uint256 amount
) internal virtual {
// 检查owner地址不为0地址
require(owner != address(0), "ERC20: approve from the zero address");
// 检查spender地址不为0地址
require(spender != address(0), "ERC20: approve to the zero address");
// 修改spender的来自owner的授权额度为amount
_allowances[owner][spender] = amount;
// 抛出事件
emit Approval(owner, spender, amount);
}
foundry代码验证:
contract ERC20Test is Test {
MockERC20 private _testing = new MockERC20("test name", "test symbol");
address private owner = address(1);
address private spender = address(2);
event Approval(address indexed owner, address indexed spender, uint value);
function test_ApproveAndIncreaseAllowanceAndDecreaseAllowance() external {
// 1. approve()
assertEq(_testing.allowance(owner, spender), 0);
vm.expectEmit(true, true, false, true, address(_testing));
emit Approval(owner, spender, 1024);
vm.prank(owner);
_testing.approve(spender, 1024);
assertEq(_testing.allowance(owner, spender), 1024);
// revert with 0 address as owner or spender
vm.expectRevert("ERC20: approve from the zero address");
vm.prank(address(0));
_testing.approve(spender, 0);
vm.expectRevert("ERC20: approve to the zero address");
_testing.approve(address(0), 0);
// 2. increaseAllowance()
vm.expectEmit(true, true, false, true, address(_testing));
emit Approval(owner, spender, 1024 + 1);
vm.prank(owner);
_testing.increaseAllowance(spender, 1);
assertEq(_testing.allowance(owner, spender), 1024 + 1);
// revert with 0 address as owner or spender
vm.expectRevert("ERC20: approve from the zero address");
vm.prank(address(0));
_testing.increaseAllowance(spender, 0);
vm.expectRevert("ERC20: approve to the zero address");
_testing.increaseAllowance(address(0), 0);
// 3. decreaseAllowance()
vm.expectEmit(true, true, false, true, address(_testing));
emit Approval(owner, spender, 1025 - 2);
vm.prank(owner);
_testing.decreaseAllowance(spender, 2);
assertEq(_testing.allowance(owner, spender), 1025 - 2);
// revert with 0 address as owner or spender
vm.expectRevert("ERC20: approve from the zero address");
vm.prank(address(0));
_testing.decreaseAllowance(spender, 0);
vm.expectRevert("ERC20: approve to the zero address");
_testing.decreaseAllowance(address(0), 0);
}
}
transfer(address to, uint256 amount)
:IERC20中的方法,调用者给to地址转账amount数量的ERC20 token。注:to地址不能为0地址,且调用者的ERC20 token余额必须不可小于转账数量amount;
transferFrom(address from, address to, uint256 amount)
: IERC20中的方法,调用者(spender)将from地址名下数量为amount的ERC20 token转移给to地址。 要求spender具有from的授权数量>=amount。注:转账成功后,spender来自from的授权额度也会同步减少amount。
function transfer(address to, uint256 amount) public virtual override returns (bool) {
// 获取调用者地址,即from地址
address owner = _msgSender();
// 调用_transfer()方法进行转账的操作
_transfer(owner, to, amount);
// 转账成功返回true
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual override returns (bool) {
// 获取调用者地址(即spender)
address spender = _msgSender();
// 调用_spendAllowance()进行授权的相关操作
_spendAllowance(from, spender, amount);
// 调用_transfer()进行转账的相关操作
_transfer(from, to, amount);
// 转账成功后返回true
return true;
}
// 从from名下转移数量为amount的ERC20 token给to地址。可以在该internal函数中根据业务需求增加转账扣费或代币削减机制。
// 注:
// 1. from和to都不可为0地址
// 2. from的余额要>=amount
function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
// 要求from地址不为0地址
require(from != address(0), "ERC20: transfer from the zero address");
// 要求to地址不为0地址
require(to != address(0), "ERC20: transfer to the zero address");
// 执行hook函数_beforeTokenTransfer(),可在该方法中自定义转账前操作逻辑
_beforeTokenTransfer(from, to, amount);
// 获取from当前余额
uint256 fromBalance = _balances[from];
// 要求from的余额足以支付这比转账
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
// 关闭solidity 0.8版本的安全数学检查
unchecked {
// 更新from转账后余额
_balances[from] = fromBalance - amount;
// 更新to转账后余额。该处加法一定不会发生溢出,因为ERC20 token的发行总量是固定不变的,该处加法的结果一定会被控制在total supply之内
_balances[to] += amount;
}
// 抛出Transfer事件
emit Transfer(from, to, amount);
// 执行hook函数_afterTokenTransfer(),可在该方法中自定义转账后操作逻辑
_afterTokenTransfer(from, to, amount);
}
// spender消耗掉来自owner的授权额度,数量为amount
// 注:
// 1. spender获得了来自owner的无限授权(即授权额度为type(uint256).max),在此函数中将无任何更新;
// 2. 如果spender来自owner的授权额度<amount,revert
function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal virtual {
// 获取spender当前来自owner的授权额度
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
// 如果spender当前来自owner的授权额度不是type(uint256).max
// 要求当前额度值>=amount
require(currentAllowance >= amount, "ERC20: insufficient allowance");
// 关闭solidity 0.8版本的安全数学检查
unchecked {
// 更新spender来自owner的授权额度为currentAllowance - amount,即原额度消耗掉amount
_approve(owner, spender, currentAllowance - amount);
}
}
}
foundry代码验证:
contract ERC20Test is Test {
MockERC20 private _testing = new MockERC20("test name", "test symbol");
address private owner = address(1);
address private spender = address(2);
event Transfer(address indexed from, address indexed to, uint value);
event Approval(address indexed owner, address indexed spender, uint value);
function test_TransferAndTransferFrom() external {
// 1. transfer()
address to = address(3);
_testing.mint(owner, 100);
vm.expectEmit(true, true, false, true, address(_testing));
emit Transfer(owner, to, 1);
vm.prank(owner);
_testing.transfer(to, 1);
assertEq(_testing.balanceOf(owner), 100 - 1);
assertEq(_testing.balanceOf(to), 1);
// revert with 0 address as from or to
vm.expectRevert("ERC20: transfer from the zero address");
vm.prank(address(0));
_testing.transfer(to, 0);
vm.expectRevert("ERC20: transfer to the zero address");
_testing.transfer(address(0), 0);
// revert with insufficient balance
vm.expectRevert("ERC20: transfer amount exceeds balance");
vm.prank(owner);
_testing.transfer(to, 99 + 1);
// 2. transferFrom()
// revert if allowance < amount
vm.prank(owner);
_testing.approve(spender, 10);
vm.expectRevert("ERC20: insufficient allowance");
vm.prank(spender);
_testing.transferFrom(owner, to, 11);
// revert if amount > owner's balance
uint balance = _testing.balanceOf(owner);
vm.prank(owner);
_testing.approve(spender, balance + 1);
vm.expectRevert("ERC20: transfer amount exceeds balance");
vm.prank(spender);
_testing.transferFrom(owner, to, balance + 1);
// revert with 0 address as owner or spender or to
vm.expectRevert("ERC20: approve from the zero address");
_testing.transferFrom(address(0), to, 0);
vm.prank(address(0));
vm.expectRevert("ERC20: approve to the zero address");
_testing.transferFrom(owner, to, 0);
vm.expectRevert("ERC20: transfer to the zero address");
vm.prank(spender);
_testing.transferFrom(owner, address(0), 0);
// pass with emit
uint balanceOwner = _testing.balanceOf(owner);
uint balanceTo = _testing.balanceOf(to);
vm.prank(owner);
_testing.approve(spender, 10);
vm.expectEmit(true, true, false, true, address(_testing));
emit Approval(owner, spender, 10 - 9);
emit Transfer(owner, to, 9);
vm.prank(spender);
_testing.transferFrom(owner, to, 9);
assertEq(_testing.balanceOf(owner), balanceOwner - 9);
assertEq(_testing.balanceOf(to), balanceTo + 9);
// no approval update with infinite allowance
vm.prank(owner);
_testing.approve(spender, type(uint).max);
assertEq(_testing.allowance(owner, spender), type(uint).max);
vm.prank(spender);
_testing.transferFrom(owner, to, 10);
assertEq(_testing.allowance(owner, spender), type(uint).max);
}
}
ps: 本人热爱图灵,热爱中本聪,热爱V神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!
公众号名称:后现代泼痞浪漫主义奠基人
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!