Appearance
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来实现基本接口,并设置了name和symbol,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,合约设置的decimals
是18
时,那么实际的代码就是:
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版本开始,上述的写法不仅不鼓励,而且是禁止的。totalSupply
和balances
会作为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 合约及其背后的一些设计原则,并且您可以将它们应用到您自己的智能合约中。