本文通过创建一个代币深入讲解 ERC20。
ERC20 代币标准
第一个标准由 Fabian Vogelsteller 于 2015 年 11 月以 ethereum request for Comments(ERC)引入,它被自动分配到 GitHub 第 20 个议题,所以叫“ERC20 代币”。目前绝大多数代币都基于 ERC20 标准。ERC20 后来变成了以太坊改进提案 20(EIP-20),但是大部分仍然使用它最初的名字,ERC20。
ERC20 是一个同质化代币标准,意思是不同的 ERC20 代币是可互换的并且不具有独特属性。
ERC20 标准[2]为实现代币的合约定义了一个通用接口,这样任何兼容的代币都可以用同样的方式访问和使用。这个接口由许多必须在每次实现中都出现的函数构成,以及一些开发者可能添加的可选函数和属性。
ERC20 需要的函数和事件
一个 ERC20 的代币合约必须至少提供下面这些函数和事件:
totalSupply
: 返回当前代币总量,可以是一个固定值或者变量。
balanceOf
:返回给定地址的代币余额
transfer
: 从执行转账的地址余额中将指定数量的代币转移到指定地址。
transferFrom
: 从一个账户到另一个账户,指定发送者,接收者和转移的代币数量。与approve
结合使用。
approve
: 指定一个被委托地址和委托代币数量,被委托地址可以在不超过委托数量的前提下多次从委托账户转移代币。
allowance
: 给定一个所有者地址和一个被委托地址,返回被委托代币余额。
Transfer: 在成功转移(调用transfer
或者transferFrom
)后触发的事件(即使转移数量为 0)。
Approval: 成功调用approve
的事件日志。
ERC20 可选函数
name
: 返回代币的可读名称(如“US Dollars”)。
symbol
: 返回代币的可读符号(如“USD”)。
decimals
: 返回代币数量的小数点位数。例如,如果decimals
为 2,表示小数点后 2 位。
ERC20 接口是用 Solidity 定义的。
下面是 Solidity 的 ERC20 接口规范:
contract ERC20 {
function totalSupply() constant returns (uint theTotalSupply);
function balanceOf(address _owner) constant returns (uint balance);
function transfer(address _to, uint _value) returns (bool success);
function transferFrom(address _from, address _to, uint _value) returns
(bool success);
function approve(address _spender, uint _value) returns (bool success);
function allowance(address _owner, address _spender) constant returns
(uint remaining);
event Transfer(address indexed _from, address indexed _to, uint _value);
event Approval(address indexed _owner, address indexed _spender, uint _value);
}
ERC20 数据结构
如果你检查任何一个 ERC20 实现,你会发现它包含两个数据结构,一个用来跟踪余额(balance),另一个用来跟踪委托代币余额(allowance)。在 Solidity 中,都是用数据映射实现的。
第一个数据映射允许代币合约跟踪谁拥有代币。每次交易都是从一个余额扣除同时在另一个余额增加:
mapping(address => uint256) balances;
第二个数据结构是委托代币余额(allowance)的数据映射。正如我们将在下一节看到的,ERC20 代币所有者可以让一个被委托者花费自己余额中一定数量的代币(allowance) 。
ERC20 合约用一个二维映射跟踪委托代币余额,其主键是代币所有者的地址,映射到被委托地址和对应的委托代币余额:
mapping (address => mapping (address => uint256)) public allowed;
ERC20 工作流程:“transfer” 和 “approve + transferFrom”
ERC20 代币标准有两个交易函数。你可能想知道为什么。
ERC20 允许两种不同的工作流程。第一种是一笔交易,使用transfer
函数的的简单流程。这个流程用于一个钱包发送代币到另一个钱包。
执行转账合约非常简单。如果 Alice 想要发送 10 个代币给 Bob,她的钱包会发送一笔交易到代币合约的地址,调用transfer
函数,并且参数为 Bob 的地址和 10。代币合约修改 Alice 的余额(-10)和 Bob 的余额(+10),然后发出一个Transfer
事件。
第二种流程是两笔交易,approve
+transferFrom
。这个流程允许代币所有者将控制权委托给另一个地址。通常用于将控制权委托给一个分配代币的合约,也可以被交易所使用。
例如,如果一个公司正在为 ICO 发售代币,他们可以委托一个众筹合约地址来分发一定数量的代币。这个众筹合约可以通过transferFrom
将代币合约所有者的余额转给每一个代币买家,如下图所示。
ERC20 workflow注意:首次代币发行(ICO)是公司或者组织为了筹集资金而出售代币的众筹机制。这个术语源自首次公开募股(IPO),这是上市公司在证券交易所向投资者出售股票的过程。与高度监管的 IPO 市场不同,ICO 是开放的、全球化的、混乱的。本文对 ICO 的示例和解释并非对此类筹款活动的认可。
ERC20 实现
虽然大约 30 行 Solidity 代码就可以实现一个 ERC20 代币,但大部分的实现都是更复杂的。这是为了解决潜在的漏洞。EIP-20 标准提到两种实现:
Consensys EIP20[3] —— 一种简单且易读的 ERC20 代币的实现。OpenZeppelin StandardToken[4] —— 这个实现兼容 ERC20,并且有额外安全措施。它形成了 OpenZeppelin 库的基础,可以实现更复杂 ERC20 代币,如筹款上限,拍卖,期权等功能。
发起自己的 ERC20 代币
接下来我们创建并发起自己的代币。下面的例子,将使用 Truffle 框架。假设你已经安装了 Truffle,如果没有安装,请用 npm 安装:
npm i truffle
假设我们的代币叫“Mastering Ethereum Token”,我们用符号“MET”代表它。
注意:你可以在这里[5]找到这个例子。
首先,我们创建并初始化一个 Truffle 项目目录。运行下面 4 个命令并接受所有默认答案:
$ mkdir METoken
$ cd METoken
METoken $ truffle init
METoken $ npm init
你现在应该有下面的目录结构了:
METoken/
+---- contracts
| `---- Migrations.sol
+---- migrations
| `---- 1_initial_migration.js
+---- package.json
+---- test
`---- truffle-config.js
编辑truffle-config.js
配置文件,配置 Truffle 环境,或者复制下面的示例:
// Install dependencies:
// npm init
// npm install --save-dev dotenv truffle-wallet-provider ethereumjs-wallet
// Create .env in project root, with keys:
// ROPSTEN_PRIVATE_KEY="123abc"
// MAINNET_PRIVATE_KEY="123abc"
require('dotenv').config();
const Web3 = require("web3");
const web3 = new Web3();
const WalletProvider = require("truffle-wallet-provider");
const Wallet = require('ethereumjs-wallet');
var mainNetPrivateKey = new Buffer(process.env["MAINNET_PRIVATE_KEY"], "hex")
var mainNetWallet = Wallet.fromPrivateKey(mainNetPrivateKey);
var mainNetProvider = new WalletProvider(mainNetWallet, "https://mainnet.infura.io/");
var ropstenPrivateKey = new Buffer(process.env["ROPSTEN_PRIVATE_KEY"], "hex")
var ropstenWallet = Wallet.fromPrivateKey(ropstenPrivateKey);
var ropstenProvider = new WalletProvider(ropstenWallet, "https://ropsten.infura.io/");
module.exports = {
networks: {
dev: { // Whatever network our local node connects to
network_id: "*", // Match any network id
host: "localhost",
port: 8545,
},
mainnet: { // Provided by Infura, load keys in .env file
network_id: "1",
provider: mainNetProvider,
gas: 4600000,
gasPrice: web3.utils.toWei("20", "gwei"),
},
ropsten: { // Provided by Infura, load keys in .env file
network_id: "3",
provider: ropstenProvider,
gas: 4600000,
gasPrice: web3.utils.toWei("20", "gwei"),
},
kovan: {
network_id: 42,
host: "localhost", // parity --chain=kovan
port: 8545,
gas: 5000000
},
ganache: { // Ganache local test RPC blockchain
network_id: "5777",
host: "localhost",
port: 7545,
gas: 6721975,
}
}
};
-truffle-config.js-
如果你用示例truffle-config.js
,记住在包含你的测试私钥的METoken
文件夹中创建一个.env
文件,以便在以太坊公共测试网(如 Ropsten or Kovan)上部署和测试。你可以从 MetaMask 导出测试网私钥。
这时,你的目录应该是这样的:
METoken/
+---- contracts
| `---- Migrations.sol
+---- migrations
| `---- 1_initial_migration.js
+---- package.json
+---- test
+---- truffle-config.js
`---- .env *new file*
警告
只能使用没有在以太坊主网上持有资产的测试密钥或者测试助记词。切勿将真正持有资产的密钥用于测试。
在我们的示例中,我们将导入 OpenZeppelin 库,这个库实现了一些重要的安全检查并且容易扩展:
$ npm install openzeppelin-solidity@1.12.0
+ openzeppelin-solidity@1.12.0
added 1 package from 1 contributor and audited 2381 packages in 4.074s
openzeppelin-solidity
包会在node_modules
目录下添加大约 250 个文件。OpenZeppelin 库并不仅仅包含 ERC20 代币,我们只会使用其中一小部分。
接下来,开始写代币合约。创建一个新文件,METoken.sol
,并从下面复制示例[6]代码。
pragma solidity ^0.4.21;
import 'openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol';
contract METoken is StandardToken {
string public constant name = 'Mastering Ethereum Token';
string public constant symbol = 'MET';
uint8 public constant decimals = 2;
uint constant _initial_supply = 2100000000;
function METoken() public {
totalSupply_ = _initial_supply;
balances[msg.sender] = _initial_supply;
emit Transfer(address(0), msg.sender, _initial_supply);
}
}
-METoken.sol-
METoken.sol
合约——实现 ERC20 代币的 Solidity 合约,非常简单,因为它从 OpenZeppelin 库继承了所有功能。
示例 1. METoken.sol
: 实现 ERC20 代币的 Solidity 合约
这里,我们定义了可选变量名,符号,和小数位数,也定义了一个_initial_supply
变量——设为 2100 万个代币,代币数量可以细分到小数点后 2 位,也就是总共 21 亿份。在合约的初始化函数(构造函数)中我们设置totalSupply
等于_initial_supply
,并且将所有_initial_supply
全部分配给创建METoken
合约的账户(msg.sender
)的余额。
现在我们用 truffle 来编译 METoken 代码:
$ truffle compile
Compiling ./contracts/METoken.sol...
Compiling ./contracts/Migrations.sol...
Compiling openzeppelin-solidity/contracts/math/SafeMath.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/BasicToken.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/ERC20.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/ERC20Basic.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol...
如你所见,truffle 编译了 OpenZeppelin 库的必要依赖。
接下来我们编写一个迁移脚本来部署METoken
合约。在METoken/migrations
文件夹中创建一个新文件2_deploy_contracts.js
。复制下面示例[7]代码:
var METoken = artifacts.require("METoken");
module.exports = function(deployer) {
// Deploy the METoken contract as our only task
deployer.deploy(METoken);
};
-2_deploy_contracts.js-
在部署到以太坊测试网之前,我们先启动一个本地区块链来测试。可以从命令行ganache-cli
或者图形用户界面来启动ganache[8]区块链。
ganache 启动,我们就可以部署 METoken 合约并且看到是否一切正常:
$ truffle migrate --network ganache
Using network 'ganache'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0xb2e90a056dc6ad8e654683921fc613c796a03b89df6760ec1db1084ea4a084eb
Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying METoken...
... 0xbe9290d59678b412e60ed6aefedb17364f4ad2977cfb2076b9b8ad415c5dc9f0
METoken: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...
在 ganache 控制台,我们可以看到已经创建了四笔新的交易:
-ganache-用 Truffle 控制台与 METoken 交互
我们可以通过 truffle 控制台在 ganache 区块链上与合约交互。这是一个交互式 JavaScript 环境,提供了对 truffle 环境的访问,并通过 web3 访问区块链。下面,我们将 truffle 控制台与 ganache 区块链连接:
$ truffle console --network ganache
truffle(ganache)>
truffle(ganache)>
表明我们已经连接到 ganache 区块链,并且已经准备好输入命令。truffle 控制台支持所有 truffle 命令,所以我们可以从控制台编译和迁移。
我们已经运行了命令,所以让我们直接进入到合约本身。METoken
合约在 truffle 环境中就像一个 JavaScript 对象。在提示符的地方输入 METoken,就会清除整个合约定义:
truffle(ganache)> METoken
{ [Function: TruffleContract]
_static_methods:
[...]
currentProvider:
HttpProvider {
host: 'http://localhost:7545',
timeout: 0,
user: undefined,
password: undefined,
headers: undefined,
send: [Function],
sendAsync: [Function],
_alreadyWrapped: true },
network_id: '5777' }
METoken
对象也揭露了几个属性,如合约地址(因为是用 migrate 命令部署的):
truffle(ganache)> METoken.address
'0x345ca3e014aaf5dca488057592ee47305d9b3e10'
如果我们想要与部署的合约交互,我们必须使用异步调用,以 JavaScript “promise” 的形式。用 deployed 函数来获取合约实例,然后调用totalSupply
函数:
truffle(ganache)> METoken.deployed().then(instance => instance.totalSupply())
BigNumber { s: 1, e: 9, c: [ 2100000000 ] }
接下来,让我们用 ganache 创建的账户来检查 METoken 余额,并且发送一些 METoken 到另一个地址。首先,获取账户地址:
truffle(ganache)> let accounts
undefined
truffle(ganache)> web3.eth.getAccounts((err,res) => { accounts = res })
undefined
truffle(ganache)> accounts[0]
'0x627306090abab3a6e1400e9345bc60c78a8bef57'
账户列表包含 ganache 创建的所有账户,accounts[0]
是部署METoken
合约的账户。它应该有METoken
余额的,因为我们的METoken
构造函数将所有 token 给到了这个地址。我们检查一下:
truffle(ganache)> METoken.deployed().then(instance =>
{ instance.balanceOf(accounts[0]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 9, c: [ 2100000000 ] }
最后,通过调用合约的transfer
函数从accounts[0]
转移 1000.00 个 METoken 到accounts[1]
:
truffle(ganache)> METoken.deployed().then(instance =>
{ instance.transfer(accounts[1], 100000) })
undefined
truffle(ganache)> METoken.deployed().then(instance =>
{ instance.balanceOf(accounts[0]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 9, c: [ 2099900000 ] }
undefined
truffle(ganache)> METoken.deployed().then(instance =>
{ instance.balanceOf(accounts[1]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 5, c: [ 100000 ] }
提示:METoken 可以精确到小数点后 2 位,意思是 1 个 METoken 在合约中其实是 100 份。当我们转移 1000 个 METoken 时,我们在调用
transfer
函数时指定的值是 100000
如你所见,在会话中,accounts[0]
现在有 20,999,000 个 MET,accounts[1]
有 1000 个 MET。
向合约地址发送 ERC20 代币
到目前为止,我们已经创建了一个 ERC20 代币并从一个账户发送了一些代币到另一个账户。前面我们用来演示的账户都是[外部账户(external owned accouts)](https://learnblockchain.cn/article/320 "外部账户(external owned accouts "外部账户(external owned accouts)")"),意思是由私钥[9]控制的账户,不是一个合约。如果我们发送 MET 到一个合约地址又会发生什么呢?
首先,我们在测试环境部署另一个合约。这个例子,我们将直接用水龙头合约Faucet.sol
。将它复制到contracts
目录下,这样就把它添加到 METoken 项目下。现在目录是这样的:
METoken/
+---- contracts
| +---- Faucet.sol
| +---- METoken.sol
| `---- Migrations.sol
还要再添加一个迁移,将Faucet
和METoken
分开部署:
var Faucet = artifacts.require("Faucet");
module.exports = function(deployer) {
// Deploy the Faucet contract as our only task
deployer.deploy(Faucet);
};
在 truffle 控制台编译并迁移合约:
$ truffle console --network ganache
truffle(ganache)> compile
Compiling ./contracts/Faucet.sol...
Writing artifacts to ./build/contracts
truffle(ganache)> migrate
Using network 'ganache'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0x89f6a7bd2a596829c60a483ec99665c7af71e68c77a417fab503c394fcd7a0c9
Migrations: 0xa1ccce36fb823810e729dce293b75f40fb6ea9c9
Saving artifacts...
Running migration: 2_deploy_contracts.js
Replacing METoken...
... 0x28d0da26f48765f67e133e99dd275fac6a25fdfec6594060fd1a0e09a99b44ba
METoken: 0x7d6bf9d5914d37bcba9d46df7107e71c59f3791f
Saving artifacts...
Running migration: 3_deploy_faucet.js
Deploying Faucet...
... 0x6fbf283bcc97d7c52d92fd91f6ac02d565f5fded483a6a0f824f66edc6fa90c3
Faucet: 0xb18a42e9468f7f1342fa3c329ec339f254bc7524
Saving artifacts...
赞,现在我们向 Faucet 合约发送 MET :
truffle(ganache)> METoken.deployed().then(instance =>
{ instance.transfer(Faucet.address, 100000) })
truffle(ganache)> METoken.deployed().then(instance =>
{ instance.balanceOf(Faucet.address).then(console.log)})
truffle(ganache)> BigNumber { s: 1, e: 5, c: [ 100000 ] }
我们已经将 1000MET 转给了 Faucet 合约。现在,我们要如何取出这些代币呢?
记住,Faucet.sol
是一个非常简单的合约。它只有一个用来提取以太币的函数withdraw
,没有用来提取 MET 的函数,或者任何其他 ERC20 代币。如果我们用withdraw
,它就会尝试发送以太币,但是因为 Faucet 没有以太币余额,所以会失败。
METoken 合约知道 Faucet 有余额,但是要转移这些余额唯一的办法就是让 Faucet 合约调用METoken
的transfer
函数。
下一步怎么办,没有办法了。发送给 Faucet 的 MET 永远卡住了。只有 Faucet 合约可以转移代币,但是 Faucet 合约没有调用 ERC20 代币合约的 transfer 函数的代码。
或许你已经预料到这个问题了,也有可能,你没有。事实上,数百名以太坊用户意外的将各种代币转移到没有 ERC20 功能的合约,据估计,这些代币价值超过 250 万美元(在写这篇文章时),已经像上面的例子一样永远被卡住,永远丢失了。
ERC20 代币用户在交易中无意丢失代币的一个原因,是他们试图将代币转移到一个交易所或者其他服务,以为可以简单的将代币发送到从交易所网站上复制的以太坊地址,然而,很多交易所发布的接收地址其实是一个合约!这些合约只接收以太币,而不是 ERC20 代币,通常这些资金会被清扫到他们的“冷藏库”或者其他中心化钱包。尽管很多警告说“不要将代币发送到这个地址”,依然有很多代币这样丢失。
ERC20 代币的问题
ERC20 标准的使用确实具有突破性,已经推出了上千种代币,既有新功能的实现,又有如众筹拍卖和 ICO 等各种资金筹集。然而,正如我们在前面向合约地址发送代币时所看到的,它有一些潜在风险。
ERC 代币一个不太明显的问题,揭露了代币和以太币之间的细微差异。以太币是通过以接收地址为目标的交易进行转移的,代币转移发生在代币合约的状态中,以代币合约作为目标,而不是接收者的地址。代币合约跟踪余额并触发事件。在代币转移中,实际没有交易发送给代币接收者,接收者的地址只是被添加到代币合约的映射。向一个地址发送以太币的交易会改变地址状态。转移代币到一个地址的交易只会改变代币合约的状态,而不是接收者地址的状态。即使 ERC20 代币的钱包也不会知道代币余额,除非用户特地添加一个代币合约来“看”。一些钱包会“看”主流代币合约,来检查它们所控制的地址持有的余额,但是这仅限于现有 ERC20 合约的小部分。
事实上,用户并不会想要跟踪所有可能的 ERC20 代币合约的所有余额。很多 ERC20 代币更像是垃圾邮件,而不是可用的代币。为了吸引用户,他们会自动为有以太币活跃的账户创建余额。如果你有一个长期活跃的以太坊地址,尤其如果它是在预售中创建的,你就会发现它充满了不知从哪里冒出来的垃圾代币。当然,这个地址并不是真的充满了代币,那只是有你的地址的代币合约。只有在区块浏览器看到这些代币合约或者你的钱包查看你的地址时,你才会看到这些余额。
代币的行为方式与以太币不同。以太币是由 send 函数发送并且由合约中的 payable 函数或者外部地址接收。代币是用只存在于 ERC20 合约中的transfer
或 approve
和 transferFrom
函数发送,并且不会在接收合约触发任何 payable 函数(至少在 ERC20 中)。代币在功能上是像以太币一样的加密货币,但是他们的一些差异打破了这种幻想。
考虑另一个问题。要发送以太币或者使用任何以太坊合约,你需要以太币来支付 gas。发送代币,你也需要以太币。你不能用代币为交易支付 gas,并且代币合约也不能为你支付 gas。这可能会在不久的将来有所改变,但同时也会导致一些奇怪的用户体验。
原文:https://betterprogramming.pub/creating-erc20-token-on-ethereum-35e109dd96e0[10]
参考资料
[1]aisiji: https://learnblockchain.cn/people/3291
[2]ERC20标准: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
[3]Consensys EIP20: https://github.com/ConsenSys/Tokens/blob/master/contracts/eip20/EIP20.sol
[4]OpenZeppelin StandardToken: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v1.12.0/contracts/token/ERC20/StandardToken.sol
[5]这里: https://github.com/ac12644/METoken.git
[6]示例: https://github.com/ac12644/METoken/blob/main/contracts/METoken.sol
[7]示例: https://github.com/ac12644/METoken/blob/main/migrations/2_deploy_contracts.js
[8]ganache: https://learnblockchain.cn/article/3501
[9]私钥: https://learnblockchain.cn/article/3624
[10]https://betterprogramming.pub/creating-erc20-token-on-ethereum-35e109dd96e0: https://betterprogramming.pub/creating-erc20-token-on-ethereum-35e109dd96e0
本文作者:aisiji[1]