Skip to content

ERC20

ERC20合约用来追踪可替换token:每个token与其他token都是一样的;任何token都没有独特的权利和行为。这让ERC20的token可以用来作为可交易资产,投票权,质押等等。
OpenZeppelin提供了很多ERC20相关的合约,你可以在API中获得更多详细的信息和用法。

构建ERC20的token合约

使用合约,我们可以轻易的创建自己的ERC20 token,用于一款名叫“GOLD(GLD)”的虚拟游戏。
以下是GLD token的代码:

solidity
// 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);  
    }  
}  

我们的合约通常通过继承来创建,在这里我们使用了ERC20来实现基本接口,并设置了namesymbol,decimals作为可选项。另外,我们设置了token的initialSupply参数,所有的代币将会分配给部署此合约的地址。

提示

关于更多关于erc20的供应机制,请参考 生成ERC20供应

完成部署后,我们可以查询部署合约账号中的余额:

sh
> GLDToken.balanceOf(deployerAddress)
1000000000000000000000

我们还可以将这些token转给其他账户地址:

sh
> GLDToken.transfer(otherAddress, 300000000000000000000)
> GLDToken.balanceOf(otherAddress)
300000000000000000000
> GLDToken.balanceOf(deployerAddress)
700000000000000000000

关于decimals

通常来说,你可以随意的分割你所拥有的token:比如你有5 GLD,你可以送1.5 GLD给你的朋友,自己保留3.5 GLD。不幸的是,Solidity 和 EVM不允许这样做:它只允许使用整数,也就是说你可以发送1或者2 个token,但不能是1.5个。
为了应对这个问题,ERC20提供了decimals参数,它可以指定token的精度是多少位。如果要发送1.5个GLD,那么精度至少是1。
这是怎么回事呢?其实很简单:token合约会使用更大的整型数来存储token值,比如50代表5 GLD,传送15就意味着传送1.5 GLD
我们要清楚,decimals只是为了显示目的而设置的。合约内部的所有计算,还是以整型数来运行的。我们的用户界面(钱包,交易信息等)的显示,要根据decimals的值来进行换算。每个账户的总 GLD token数量需要乘以 10的decimals次方,来获取真正的GLD数量。
你可以像效仿以太坊或一些ERC20代币,将decimals设置成18,除非你有特殊的理由,不然不要这样做。当minting tokens或者转账时,你实际发送的数据将是num GLD * (10 ** decimals)

注意

默认情况下,ERC20的合约decimals值是18。如果要设置不同的值,需要在你的合约中覆盖decimals()函数。

solidity
function decimals() public view virtual override returns (uint8) {
    return 16;
}

如果你需要发送5个token,合约设置的decimals18时,那么实际的代码就是:

solidity
transfer(recipient, 5 * (10 ** 18));

预设ERC20合约

预设ERC20合约可以通过ERC20PresetMinterPauser来实现。它可以预先设置,允许进行挖矿(create),停止所有的转账操作(pause),允许用户销毁token(destroy)。合约使用访问控制来限定挖矿和暂停合约的函数操作。合约的部署账号默认拥有挖矿和暂停的角色,也就是默认的管理员角色。
这个合约可以不编写任何Solidity代码直接部署。它可以用来做原型搭建和测试,同时在生产环境中也可以正常使用。

生成ERC20供应

在这部分教程中,我们会学习如何创建自定义的ERC20 token供应机制。我们会使用两种常用的方式来展现,如何使用OpenZeppelin合约来实现,同时你也可以在自己的智能合约开发中进行练习。
构建与以太坊上面代币Token的标准接口实现称为ERC20。其合约有着非常广泛的应用:它们统称为ERC20合约。这个合约就像标准本身一样,非常简单,并且包含了其基本要素。如果你直接部署ERC20合约的话,那么这个合约是没什么实际意义的。因为它并没有包含供应量,token如果不设置供应量就没有任何作用。
供应量的设置方法并没有在ERC20的文档里定义。每个token都可以自由的设置自己的供应机制,从最去中心化的,到最中心化的,从最简单的,到最成熟的,等等。

固定供应量

我们看一下这个例子,如果我们需要供应1000个token,在部署合约时将这些token分配给部署者。使用v1版本合约,可以编写以下代码:

solidity
contract ERC20FixedSupply is ERC20 {
    constructor() {
        totalSupply += 1000;
        balances[msg.sender] += 1000;
    }
}

但是从v2版本开始,上述的写法不仅不鼓励,而且是禁止的。totalSupplybalances会作为ERC20实现中的私有元素,你不能直接对他们进行写入操作,只能通过_mint函数来完成:

solidity
contract ERC20FixedSupply is ERC20 {
    constructor() ERC20("Fixed", "FIX") {
        _mint(msg.sender, 1000);
    }
}

在继承合约时,这样包装状态会让它变得更加安全。例如,在第一个示例中,我们必须手动保持 totalSupply 与修改后的余额同步,这很容易忘记。事实上,我们还忽略了一些东西,它也很容易忘记:那就是Transfer事件,这也是在标准中要求的,并且客户端也会依赖这个事件。第二个例子中解决了这个问题,因为它是通过调用_mint函数来完成的。

奖励矿工

内部函数_mint是构建区块的关键,它可以让我们扩展ERC20的供应机制。
接下来我们会做一个奖励挖矿者的奖励机制。在Solidity中我们可以通过全局变量block.coinbase获取到当前区块矿工的地址。有人调用mintMinerReward()函数时,我们会奖励token给这个区块的矿工。这个机制看起来有点问题,但你永远不知道这会产生怎样的动态结果,这值得我们分析和总结!

solidity
contract ERC20WithMinerReward is ERC20 {
    constructor() ERC20("Reward", "RWD") {}

    function mintMinerReward() public {
        _mint(block.coinbase, 1000);
    }
}

正如我们所见,_mint函数让这变得非常容易。

将机制模块化

合约中已经包含了一种供应机制:ERC20PresetMinterPauser。这是一种通用机制,其中一组帐户被分配了 minter 角色,授予他们调用 mint 函数的权限,即 _mint 的外部版本。
这可以用于中心化挖矿,外部拥有的帐户(即拥有一对加密密钥的人)决定创建多少供应以及为谁创建。这是一种非常合理的供应机制,比如传统资产背书的稳定币
这个有铸币权限的账户,也可以不是一个外部账户,它可以是一个实现了去信任机制的智能合约。我们可以像之前一样实现这个功能。

solidity
contract MinerRewardMinter {
    ERC20PresetMinterPauser _token;

    constructor(ERC20PresetMinterPauser token) {
        _token = token;
    }

    function mintMinerReward() public {
        _token.mint(block.coinbase, 1000);
    }
}

这个合约是通过ERC20PresetMinterPauser实例来初始化,然后把minter角色授权给这个合约,这样做也可以实现和之前例子一样的功能。这里比较有趣的是,我们使用了ERC20PresetMinterPauser,我们可以通过将角色分配给多个合同来轻松组合多种供应机制,并且是动态的来实现。

提示

关于角色和权限,请参考访问控制

自动化奖励机制

目前为止我们讲诉的都是手工编写奖励机制,ERC20也允许我们通过_beforeTokenTransfer这个hook来扩展token的核心函数。
在之前的例子中加入这个功能,我们可以用这个hook为区块链中包含的每个代币转移铸造矿工奖励。

solidity
contract ERC20WithAutoMinerReward is ERC20 {
    constructor() ERC20("Reward", "RWD") {}

    function _mintMinerReward() internal {
        _mint(block.coinbase, 1000);
    }

    function _beforeTokenTransfer(address from, address to, uint256 value) internal virtual override {
        if (!(from == address(0) && to == block.coinbase)) {
        _mintMinerReward();
        }
        super._beforeTokenTransfer(from, to, value);
    }
}

总结

我们已经看到了两种实现 ERC20 供应机制的方法:内部通过 _mint,外部通过 ERC20PresetMinterPauser。希望这可以帮助您了解如何使用 OpenZeppelin 合约及其背后的一些设计原则,并且您可以将它们应用到您自己的智能合约中。

Released under the MIT License.