ERC20协议的一些思考
近来重读ERC20 协议标准有了一些新的思考,虽然我已经进行过 2 年的 solidity 开发,我们公司也构建了自己的token
,ERC20 对我来说已经非常熟悉了,但是对简单的东西再深入思考,还是会帮助自己更加理解区块链程序的设计思想,获得新的收获。于是乎,我想把自己的思考记录下来,以便将来复盘。
ERC20 是一组接口,是以太坊上的一个协议标准,任何遵循了这个接口标准的token
都可以互相兼容,相互调用。通常我们第一印象就是用 ERC20 构建代币,因为区块链技术本身就是由比特币引爆的。其实 ERC20 不仅可以发币,也可以适用很多场景,如:在线平台信誉点
, 游戏角色技能点
, 公司股份
等。
本文将基于ERC20 协议标准和OpenZeppelin implementation进行讲解,会提取其中部分代码进行分析,给出我的思考。
下面是 ERC20 标准的所有接口:
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)
transfer
转移数量为 0 时,为什么必须视为正常的交易?请看 ERC20 标准对接口的描述:
大概没有人会注意这个问题吧...其实这本来不应该是一个问题,因为当_value
等于 0 时,无论交易成功,还是交易回滚,对交易者的余额都没有影响。所以,这个问题应该转化为:当_value
等于 0 时,回滚交易,会减少一次状态变量的修改,进而可以减少 gas 费,为什么不这么做呢?
当考虑gas优化时,也许你会思考这个问题,然而,我们设计程序时,应该优先保证交易成功,其次才是 gas 优化等其他的优化。显然,回滚交易虽然优化了gas,却引发了不必要的交易失败,没必要。
token
为什么是给msg.sender
铸造总量而不是合约本身呢?我们在CoinMarketCap随便找一个币的源码看看:
这涉及到区块链的本质,即交易。能够提出这个问题的人,一定是用 web2 的思维来思考 web3,包括以前刚入行的我自己。在 web2 的世界里,把 token 总量存储在合约,由合约进行分配,这是很自然的,一切都是中心化的。然而,这在 web3 的世界里行不通!在 web3 一切皆是交易,必须由人主动发起一笔交易才能触发分配,而合约本身不具备自动触发交易的能力。
于是乎,发token
的标准流程就是:
token
的创建者铸造出一定数量的 token,通常都是预先规划好的一个数值,即 totalsupply。ERC20 token
,想添加新的功能,比如以前是固定数额,现在想增加铸造方法增发,可以升级吗?我们只考虑项目已经运行且已经有用户的情况,如果没有用户,重新发行一个新的token就可以。token
的本质只是区块链上的一个数字映射,即 某个人
拥有了 某个数量
的映射关系。源代码如下:
mapping (address => uint256) private _balances;
所以升级,就是把所有用户的这个余额,都要迁移至新的合约,然而,这个操作只能由用户本人授权才能操作,具体步骤如下:
这不是你能控制的,用户可以迁移,也可以不迁移,你只能做运营活动来激发用户迁移的积极性
)是不是挺麻烦的?其实现在社区已经有了一整套合约升级的标准,透明代理
和uups代理
等等,如果是新开发的 token,编写可升级合约,能解决这个问题。我之后也会考虑写一篇文章,来讲述可升级合约如何编写。
transfer
方法这是 openzeppelin 的标准实现:
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;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
可以看到,transfer
最核心的内容就是操作_balances
这个状态变量,减去from
的余额,加上to
的余额。方法内部还提供了_beforeTokenTransfer
和_afterTokenTransfer
两个方法,你基于这个标准实现自己的 token 时,也可以实现这两个方法,用来在 token 转移前,转移后做一些定制化操作。
另外,unchecked
是一种优化 gas 的方法,在 solidity 0.8.0 以上的版本中,没有了数学溢出的 bug,每次数学计算编译器会自动增加检查的操作,带来了更大的安全性,但在这里的数学计算明确不会溢出,就可以用unchecked
包起来禁用检查,从而减少操作,减少 gas 费。
token
的标准实现现在,我们只需要基于openzeppelin
的标准实现来定义自己的 token 即可。你自己实现所有的方法固然可以,但不建议这么做,毕竟openzeppelin
的代码都是经过安全审计,大多数人在用的,你自己实现的不一定安全。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Token is ERC20, AccessControl {
// 自定义权限名称
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() ERC20("Token", "Token") {
// 给 创建者 铸造一定数量的token
_mint(msg.sender, 1000000 * 10 ** decimals());
// 给 创建者 所有权限,如果不需要增发,即没有下面的mint方法,可以不需要授权这个功能
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
}
// [可选] 如果有增发的需求,可以加上这个方法。注意一定要加上权限控制!
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!