Web3专题(六) 深入理解 ERC20

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 标准对接口的描述:

ERC20-Transfer.png

大概没有人会注意这个问题吧...其实这本来不应该是一个问题,因为当_value等于 0 时,无论交易成功,还是交易回滚,对交易者的余额都没有影响。所以,这个问题应该转化为:当_value等于 0 时,回滚交易,会减少一次状态变量的修改,进而可以减少 gas 费,为什么不这么做呢?

当考虑gas优化时,也许你会思考这个问题,然而,我们设计程序时,应该优先保证交易成功,其次才是 gas 优化等其他的优化。显然,回滚交易虽然优化了gas,却引发了不必要的交易失败,没必要。

发行token为什么是给msg.sender铸造总量而不是合约本身呢?

我们在CoinMarketCap随便找一个币的源码看看:

ERC20铸币.png

这涉及到区块链的本质,即交易。能够提出这个问题的人,一定是用 web2 的思维来思考 web3,包括以前刚入行的我自己。在 web2 的世界里,把 token 总量存储在合约,由合约进行分配,这是很自然的,一切都是中心化的。然而,这在 web3 的世界里行不通!在 web3 一切皆是交易,必须由人主动发起一笔交易才能触发分配,而合约本身不具备自动触发交易的能力。

于是乎,发token的标准流程就是:

  1. 先给token的创建者铸造出一定数量的 token,通常都是预先规划好的一个数值,即 totalsupply。
  2. 由创建者触发交易,分配 token(这一步每家公司都不同,看具体的业务)。

已经发行的ERC20 token,想添加新的功能,比如以前是固定数额,现在想增加铸造方法增发,可以升级吗?

我们只考虑项目已经运行且已经有用户的情况,如果没有用户,重新发行一个新的token就可以。token的本质只是区块链上的一个数字映射,即 某个人 拥有了 某个数量 的映射关系。源代码如下:

mapping (address => uint256) private _balances;

所以升级,就是把所有用户的这个余额,都要迁移至新的合约,然而,这个操作只能由用户本人授权才能操作,具体步骤如下:

  1. 编写 新 token 合约,增加新功能
  2. 编写 迁移合约,提供一个迁移方法,收回旧合约的 token,同时铸造新合约的 token
  3. 由用户本人授权调用迁移方法(这不是你能控制的,用户可以迁移,也可以不迁移,你只能做运营活动来激发用户迁移的积极性

是不是挺麻烦的?其实现在社区已经有了一整套合约升级的标准,透明代理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);
    }
}
点赞 2
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
认知那些事
认知那些事
0x2b62...95a0
人立于天地之间,必然有我们的出路。