Token 标准

区块链标记在以太坊之前就已存在。在某些方面,第一块区块链货币比特币本身就是一种Token。在Ethereum之前,还在比特币和其他加密货币上开发了许多Token平台。然而,在以太坊上引入第一个Token标准导致Token爆炸。

Vitalik Buterin建议将Token作为通用可编程区块链(如以太坊)最明显和最有用的应用之一。事实上,在以太坊的第一年,经常看到Vitalik和其他人穿着印有Ethereum标志的T恤和背面的智能合约样本。这件T恤有几种变化,但最常见的是一种Token的实现。

ERC20 Token 标准

第一个标准由Fabian Vogelsteller于2015年11月引入,作为以太坊征求意见(ERC)。它被自动分配了GitHub发行号码20,从而获得了名字“ERC20 Token”。绝大多数Token目前都基于ERC20。ERC20征求意见最终成为以太坊改进建议EIP20,但大多仍以原名ERC20提及。你可以在这里阅读标准:

https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md

ERC20是_可替换Token_的标准,意味着ERC20标记的不同单元是可互换的,没有唯一属性。

ERC20标准为实现Token的合同定义了一个通用接口,这样任何兼容的Token都可以以相同的方式访问和使用。该接口包含许多必须存在于标准的每个实现中的函数,以及可能由开发人员添加的一些可选函数和属性。

ERC20 必须的函数和事件

totalSupply

返回当前存在的Token的总单位。ERC20Token可以有固定或可变的供应量。

balanceOf

给定一个地址,返回该地址的Token余额。

transfer

给定一个地址和数量,将该数量的Tokens从执行该方法的地址的余额转移到该地址。

transferFrom

给定发送者,接收人和数量,将Token从一个帐户转移到另一个帐户。与+approve+结合使用。

approve

在给定接收者地址和数量的情况下,授权该地址从发布批准的帐户执行多次转账,直到到达指定的数量。

allowance

给定一个所有者地址和一个消费者地址,返回该消费者被批准从所有者的取款的剩余金额。

Transfer event

成功转移时触发的事件(调用+transfer+或+transferFrom+)(即使对于零值转移)。

Approval event

成功调用+approve+时记录的事件。

ERC20 可选函数

name

返回Token的可读名称(例如“US Dollars”)。

symbol

返回Token的人类可读符号(例如“USD”)。

decimals

返回用于分割Token数量的小数位数。例如,如果小数为2,则将Token数除以100以获取其用户表示。

在Solidity中定义的ERC20接口

以下是在Solidity中ERC20接口规范的样子:

  1. contract ERC20 {
  2. function totalSupply() constant returns (uint theTotalSupply);
  3. function balanceOf(address _owner) constant returns (uint balance);
  4. function transfer(address _to, uint _value) returns (bool success);
  5. function transferFrom(address _from, address _to, uint _value) returns (bool success);
  6. function approve(address _spender, uint _value) returns (bool success);
  7. function allowance(address _owner, address _spender) constant returns (uint remaining);
  8. event Transfer(address indexed _from, address indexed _to, uint _value);
  9. event Approval(address indexed _owner, address indexed _spender, uint _value);
  10. }
ERC20 数据结构

如果你检查任何ERC20实现,它将包含两个数据结构,一个用于追踪余额,另一个用于追踪配额(allowances)。在Solidity中,它们使用_data mapping_实现。

第一个data mapping按拥有者实现了Token余额的内部表。这允许Token合约跟踪谁拥有Token。每次转账都是从一个余额中扣除的,并且是对另一个余额的增加。

Balances: a mapping from address (owner) to amount (balance)

  1. mapping(address => uint256) balances;

第二个数据结构是配额的data mapping。正如我们将在ERC20工作流程:“transfer”和“approve & transferFrom”中看到的那样,使用ERC20Token,Token的所有者可以将权限委托给花钱者,允许他们从所有者的余额中花费特定金额(配额)。ERC20合同通过二维映射追踪配额,主关键字是Token所有者的地址,映射到一个花费者地址和配额金额:

Allowances: a mapping from address (owner) to address (spender) to amount (allowance)

  1. mapping (address => mapping (address => uint256)) public allowed;
ERC20工作流程:“transfer”和“approve & transferFrom”

ERC20Token标准具有两种传输功能。你可能想知道为什么?

ERC20允许两种不同的工作流程。第一个是使用+transfer+函数的单次交易,简单的工作流程。这个工作流程是钱包用来将Token发送给其他钱包的工作流程。绝大多数Token交易都发生在+transfer+工作流程中。

执行转让合同非常简单。如果爱丽丝希望向鲍勃发送10个Token,她的钱包会向Token合约的地址发送一个交易,并用Bob的地址和“10”作为参数调用+transfer+函数。Token合约调整Alice的余额(-10)和Bob的余额(+10)并发出+Transfer+事件。

第二个工作流程是使用+approve+和+transferFrom+的双交易工作流程。该工作流程允许Token所有者将其控制权委托给另一个地址。它通常用于委托控制权给合约来分配Token,但它也可以被交易所使用。例如,如果一家公司为ICO出售Token,他们可以+approve+一个crowdsale合同地址来分发一定数量的Token。然后crowdsale合同可以用+transferFrom+转移给Token的每个买家。

The two-step approve & transferFrom workflow of ERC20 Tokens

Figure 1. The two-step approve & transferFrom workflow of ERC20 Tokens

对于 approve & transferFrom 工作流程,需要两个交易。假设Alice希望允许AliceICO合同将所有AliceCoin Token的50%卖给像Bob和Charlie这样的买方。首先,Alice发布AliceCoin ERC20合同,将所有AliceCoin发放到她自己的地址。然后,Alice发布可以以ether出售Token的AliceICO合同。接下来,Alice启动+approve & transferFrom+工作流程。她向AliceCoin发送一个交易,调用+approve+,参数是AliceICO的地址和+totalSupply+的50%。这将触发+Approval+事件。现在,AliceICO合同可以出售AliceCoin了。

当AliceICO从Bob那收到ether,它需要发送一些AliceCoin给Bob作为回报。在AliceICO合约内是AliceCoin和ether之间的汇率。Alice在创建AliceICO时设置的汇率决定了Bob将根据他发送给AliceICO的ether数量能得到多少Token。当AliceICO调用AliceCoin transferFrom+函数时,它将Alice的地址设置为发送者,将Bob的地址设置为接收者,并使用汇率来确定将在“value”字段中将多少AliceCoin Token传送给Bob。AliceCoin合同将余额从Alice的地址转移到Bob的地址并触发 +Transfer 事件。只要不超过Alice设定的审批限制,AliceICO合同可以调用 transferFrom 无限次数。AliceICO合同可以通过调用+allowance+函数来跟踪它能卖出多少AliceCoinToken。

ERC20 实现

虽然可以在约三十行Solidity代码中实现兼容ERC20的Token,但大多数实现都更加复杂,以解决潜在的安全漏洞。在EIP20标准中提到了两种实现:

Consensys EIP20

简单易读的ERC20兼容Token的实现。

你可以在此处阅读Consensys实现的Solidity代码: https://github.com/ConsenSys/Tokens/blob/master/contracts/eip20/EIP20.sol

OpenZeppelin StandardToken

此实现与ERC20兼容,并具有额外的安全防范措施。它构成了OpenZeppelin库的基础,实现了更复杂的与ERC20兼容的Token,包括筹款上限,拍卖,归属时间表和其他功能。

你可以在这里看到OpenZeppelin StandardToken的Solidity代码: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/Token/ERC20/StandardToken.sol

发布我们自己的ERC20Token

让我们创建并发布我们自己的Token。在这个例子中,我们将使用+truffle+ 框架(参见[truffle])。该示例假设你已经安装了+truffle+,进行了配置,并熟悉其基本操作。

我们将称之为“Mastering Ethereum Token”,标志为“MET”。

你可以在本书的GitHub仓库中找到这个例子: https://github.com/ethereumbook/ethereumbook/blob/develop/code/truffle/METoken

首先,让我们创建并初始化一个Truffle项目目录,就像我们在[truffle_project_directory]中所做的那样。运行这四个命令并接受任何问题的默认答案:

  1. $ mkdir METoken
  2. $ cd METoken
  3. METoken $ truffle init
  4. METoken $ npm init

你现在应该具有以下目录结构:

  1. METoken/
  2. ├── contracts
  3. └── Migrations.sol
  4. ├── migrations
  5. └── 1_initial_migration.js
  6. ├── package.json
  7. ├── test
  8. ├── truffle-config.js
  9. └── truffle.js

编辑+truffle.js+或+truffle-config.js+配置文件以设置+Truffle+环境,或复制我们使用的环境:

https://github.com/ethereumbook/ethereumbook/blob/develop/code/truffle/METoken/truffle-config.js

如果使用示例+truffle-config.js+,请记住在包含你的测试私钥的+METoken+文件夹中创建文件+.env+,以便在公共以太网测试网络(如ganache或Kovan)上进行测试和部署。你可以从MetaMask中导出你的测试网络私钥。

之后你的目录看起来像:

  1. METoken/
  2. ├── contracts
  3. └── Migrations.sol
  4. ├── migrations
  5. └── 1_initial_migration.js
  6. ├── package.json
  7. ├── test
  8. ├── truffle-config.js
  9. ├── truffle.js
  10. └── .env *new file*
Warning

只能使用不用于在以太坊主网络上持有资金的测试密钥或测试助记符。切勿使用持有真正金钱的钥匙进行测试。

对于我们的示例,我们将导入OpenZeppelin StandardContract,它实现了一些重要的安全检查并且易于扩展。让我们导入该库:

  1. $ npm install zeppelin-solidity
  2. + zeppelin-solidity@1.6.0
  3. added 8 packages in 2.504s

+ zeppelin-solidity 包将在 node_modules +目录下添加约250个文件。OpenZeppelin库包含的不仅仅是ERC20Token,但我们只使用它的一小部分。

接下来,让我们编写我们的Token合约。创建一个新文件+METoken.sol+并从GitHub复制示例代码:

https://github.com/ethereumbook/ethereumbook/blob/develop/code/truffle/METoken/contracts/METoken.sol

我们的合同非常简单,因为它继承了OpenZeppelin StandardToken库的所有功能:

METoken.sol : A Solidity contract implementing an ERC20 Token

  1. link:code/METoken/contracts/METoken.sol[]

在这里,我们定义可选变量+name+,symbol+和+decimals。我们还定义了一个+_initial_supply+变量,设置为2,100万个Token,以及两个小数细分(总共21亿)。在契约的初始化(构造函数)函数中,我们将+totalSupply+设置为等于+_initial_supply+,并将所有+_initial_supply+分配给创建 METoken 契约的帐户余额(msg.sender)。

我们现在使用+truffle+编译+METoken+代码:

  1. $ truffle compile
  2. Compiling ./contracts/METoken.sol...
  3. Compiling ./contracts/Migrations.sol...
  4. Compiling zeppelin-solidity/contracts/math/SafeMath.sol...
  5. Compiling zeppelin-solidity/contracts/Token/ERC20/BasicToken.sol...
  6. Compiling zeppelin-solidity/contracts/Token/ERC20/ERC20.sol...
  7. Compiling zeppelin-solidity/contracts/Token/ERC20/ERC20Basic.sol...
  8. Compiling zeppelin-solidity/contracts/Token/ERC20/StandardToken.sol...

如你所见,+truffle+包含了OpenZeppelin库的必要依赖关系,并编译了这些契约。

我们建立一个migration脚本,部署 METoken+合约。在+METoken/migrations+文件夹中创建一个新文件+2_deploy_contracts.js。从Github存储库中的示例复制内容:

https://github.com/ethereumbook/ethereumbook/blob/develop/code/truffle/METoken/migrations/2_deploy_contracts.js

以下是它包含的内容:

2_deploy_contracts: Migration to deploy METoken

  1. link:code/METoken/migrations/2_deploy_contracts.js[]
  2. var METoken = artifacts.require("METoken");
  3. module.exports = function(deployer) {
  4. // Deploy the METoken contract as our only task
  5. deployer.deploy(METoken);
  6. };

在我们部署其中一个以太坊测试网络之前,让我们开始一个本地区块链来测试一切。正如我们在[using_ganache]中所做的那样,从 ganache-cli 的命令行或从图形用户界面启动 ganache 区块链。

一旦 ganache 启动,我们就可以部署我们的METoken合约,看看是否一切都按预期工作:

  1. $ truffle migrate --network ganache
  2. Using network 'ganache'.
  3. Running migration: 1_initial_migration.js
  4. Deploying Migrations...
  5. ... 0xb2e90a056dc6ad8e654683921fc613c796a03b89df6760ec1db1084ea4a084eb
  6. Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
  7. Saving successful migration to network...
  8. ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
  9. Saving artifacts...
  10. Running migration: 2_deploy_contracts.js
  11. Deploying METoken...
  12. ... 0xbe9290d59678b412e60ed6aefedb17364f4ad2977cfb2076b9b8ad415c5dc9f0
  13. METoken: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
  14. Saving successful migration to network...
  15. ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
  16. Saving artifacts...

在+ganache+控制台上,我们应该看到我们的部署创建了4个新的交易:

METoken deployment on Ganache

Figure 2. METoken deployment on Ganache

使用Truffle控制台与METoken交互

我们可以使用+Truffle控制台+与我们的+ganache+区块链合同进行互动。这是一个交互式的JavaScript环境,可以访问Truffle环境,并通过Web3访问区块链。在这种情况下,我们将+Truffle控制台+连接到+ganache+区块链:

  1. $ truffle console --network ganache
  2. truffle(ganache)>
  1. +truffle(ganache)>+ 提示符表明我们已连接到 +ganache+区块链并准备输入我们的命令。+Truffle控制台+支持所有的Truffle命令,所以我们可以从控制台+compile+和+migrate+。我们已经运行过这些命令,所以让我们直接看看合同本身。METoken合约作为Truffle环境内的JavaScript对象存在。在提示符下键入+METoken+,它将转储整个合约定义:
  1. truffle(ganache)> METoken
  2. { [Function: TruffleContract]
  3. _static_methods:
  4. [...]
  5. currentProvider:
  6. HttpProvider {
  7. host: 'http://localhost:7545',
  8. timeout: 0,
  9. user: undefined,
  10. password: undefined,
  11. headers: undefined,
  12. send: [Function],
  13. sendAsync: [Function],
  14. _alreadyWrapped: true },
  15. network_id: '5777' }

+METoken+对象还公开几个属性,例如合同的地址(由+migrate+命令部署):

  1. truffle(ganache)> METoken.address
  2. '0x345ca3e014aaf5dca488057592ee47305d9b3e10'

如果我们想要与已部署的合同进行交互,我们必须以JavaScript“promise”的形式使用异步调用。我们使用+deployment+函数来获取合约实例,然后调用+totalSupply+函数:

  1. truffle(ganache)> METoken.deployed().then(instance => instance.totalSupply())
  2. BigNumber { s: 1, e: 9, c: [ 2100000000 ] }

接下来,让我们使用由+ganache+创建的账户来检查我们的METoken余额并将一些METoken发送到另一个地址。首先,让我们获取帐户地址:

  1. truffle(ganache)> let accounts
  2. undefined
  3. truffle(ganache)> web3.eth.getAccounts((err,res) => { accounts = res })
  4. undefined
  5. truffle(ganache)> accounts[0]
  6. '0x627306090abab3a6e1400e9345bc60c78a8bef57'

accounts 列表现在包含由+ganache+创建的所有帐户,而+account[0]+是部署了该METoken合约的帐户。它应该有METoken的余额,因为我们的METoken构造函数将全部Token提供给了创建它的地址。让我们检查:

  1. truffle(ganache)> METoken.deployed().then(instance => { instance.balanceOf(accounts[0]).then(console.log) })
  2. undefined
  3. BigNumber { s: 1, e: 9, c: [ 2100000000 ] }

最后,通过调用合约的 transfer+函数,让我们从+account[0] 向 account[1] 转移1000.00 METoken:

  1. truffle(ganache)> METoken.deployed().then(instance => { instance.transfer(accounts[1], 100000) })
  2. undefined
  3. truffle(ganache)> METoken.deployed().then(instance => { instance.balanceOf(accounts[0]).then(console.log) })
  4. undefined
  5. truffle(ganache)> BigNumber { s: 1, e: 9, c: [ 2099900000 ] }
  6. undefined
  7. truffle(ganache)> METoken.deployed().then(instance => { instance.balanceOf(accounts[1]).then(console.log) })
  8. undefined
  9. truffle(ganache)> BigNumber { s: 1, e: 5, c: [ 100000 ] }
Tip

METoken具有2位精度的小数,这意味着1个METoken在合同中是100个单位。当我们传输1000个METoken时,我们在传输函数中将该值指定为100,000。

如你所见,在控制台中,+ account [0] 现在拥有20,999,000 MET, account [1] +拥有1000 MET。

如果切换到+ganache+图形用户界面,你将看到名为+transfer+函数的交易:

METoken transfer on Ganache

Figure 3. METoken transfer on Ganache

将ERC20Token发送到合同地址

到目前为止,我们已经设置了ERC20Token并从一个帐户转移到另一个帐户。我们用于这些示范的所有账户都是外部拥有账户(EOAs),这意味着它们由私钥控制,而不是合同。如果我们将MET发送到合同地址会发生什么?让我们看看!

首先,我们将其他合约部署到我们的测试环境中。对于这个例子,我们将使用我们的第一个合同+Faucet.sol+。我们将它添加到METoken项目中,方法是将它复制到+contracts+目录。我们的目录应该是这样的:

  1. METoken/
  2. ├── contracts
  3. ├── Faucet.sol
  4. ├── METoken.sol
  5. └── Migrations.sol

我们还会添加一个migration,从+METoken+单独部署+Faucet+:

  1. var Faucet = artifacts.require("Faucet");
  2. module.exports = function(deployer) {
  3. // Deploy the Faucet contract as our only task
  4. deployer.deploy(Faucet);
  5. };

让我们从Truffle控制台编译和迁移合同:

  1. $ truffle console --network ganache
  2. truffle(ganache)> compile
  3. Compiling ./contracts/Faucet.sol...
  4. Writing artifacts to ./build/contracts
  5. truffle(ganache)> migrate
  6. Using network 'ganache'.
  7. Running migration: 1_initial_migration.js
  8. Deploying Migrations...
  9. ... 0x89f6a7bd2a596829c60a483ec99665c7af71e68c77a417fab503c394fcd7a0c9
  10. Migrations: 0xa1ccce36fb823810e729dce293b75f40fb6ea9c9
  11. Saving artifacts...
  12. Running migration: 2_deploy_contracts.js
  13. Replacing METoken...
  14. ... 0x28d0da26f48765f67e133e99dd275fac6a25fdfec6594060fd1a0e09a99b44ba
  15. METoken: 0x7d6bf9d5914d37bcba9d46df7107e71c59f3791f
  16. Saving artifacts...
  17. Running migration: 3_deploy_faucet.js
  18. Deploying Faucet...
  19. ... 0x6fbf283bcc97d7c52d92fd91f6ac02d565f5fded483a6a0f824f66edc6fa90c3
  20. Faucet: 0xb18a42e9468f7f1342fa3c329ec339f254bc7524
  21. Saving artifacts...

现在让我们将一些MET发送到 Faucet 合约:

  1. truffle(ganache)> METoken.deployed().then(instance => { instance.transfer(Faucet.address, 100000) })
  2. truffle(ganache)> METoken.deployed().then(instance => { instance.balanceOf(Faucet.address).then(console.log)})
  3. truffle(ganache)> BigNumber { s: 1, e: 5, c: [ 100000 ] }

好的,我们已将1000 MET转移到 Faucet+合约。现在,我们如何从 +Faucet 提款呢?

请记住,Faucet.sol+是一个非常简单的合同。它只有一个功能,+withdraw,这是提取_ether_。它没有提取MET或任何其他ERC20Token的功能。如果我们使用+withdraw+它将尝试发送ether,但由于Faucet还没有ether的余额,它将失败。

METoken+合约知道+Faucet+有余额,但它可以转移该余额的唯一方法是它从合约地址收到+transfer+调用。无论如何,我们需要让+Faucet 合约调用+MET+中的+transfer+函数。

如果你在思考下一步该做什么,不必了。这个问题没有解决办法。MET发送到+Faucet+将永远卡住。只有+Faucet+合约可以转让它,+Faucet+合约没有调用ERC20Token合约的+transfer+函数的代码。

也许你预料到了这个问题。最有可能的是,你没有。实际上,数百名以太坊用户也无意将各种Token转让给没有任何ERC20能力的合同。据估计,价值超过250万美元的Token被这样“卡住”,并且永远丢失。

ERC20Token的用户可能无意中在转移中丢失其Token的方式之一是当他们尝试转移到交易所或其他服务时。他们从交易所的网站上复制以太坊地址,认为他们可以简单地向其发送Token。但是,许多交易所都公布实际上是合同的接收地址!这些合同具有许多不同的功能,通常将发送给他们的所有资金清扫到“冷存储”或另一个集中的钱包。尽管有许多警告说“不要将Token发送到这个地址”,但许多Token会以这种方式丢失。

演示 approve & transferFrom 流程

我们的+Faucet+合同无法处理ERC20Token。使用+transfer+函数发送Token给它,会导致这些Token的丢失。我们重写合同,并处理ERC20Token。具体来说,我们将把它变成一个Faucet,将MET发给任何询问的人。

对于这个例子,我们制作了Truffle项目目录的副本,将其称为 METoken_METFaucet,初始化Truffle,npm,安装OpenZeppelin依赖项并复制+METoken.sol+合同。有关详细说明,请参阅我们的第一个示例发布我们自己的ERC20Token

现在,让我们创建一个新的Faucet合同,称之为+METFaucet.sol+。它看起来像这样:

METFaucet.sol: a faucet for METoken

  1. include::code/METoken_METFaucet/contracts/METFaucet.sol
  2. // Version of Solidity compiler this program was written for
  3. pragma solidity ^0.4.19;
  4. import 'zeppelin-solidity/contracts/Token/ERC20/StandardToken.sol';
  5. // A faucet for ERC20 Token MET
  6. contract METFaucet {
  7. StandardToken public METoken;
  8. address public METOwner;
  9. // METFaucet constructor, provide the address of METoken contract and
  10. // the owner address we will be approved to transferFrom
  11. function METFaucet(address _METoken, address _METOwner) public {
  12. // Initialize the METoken from the address provided
  13. METoken = StandardToken(_METoken);
  14. METOwner = _METOwner;
  15. }
  16. function withdraw(uint withdraw_amount) public {
  17. // Limit withdrawal amount to 10 MET
  18. require(withdraw_amount <= 1000);
  19. // Use the transferFrom function of METoken
  20. METoken.transferFrom(METOwner, msg.sender, withdraw_amount);
  21. }
  22. // REJECT any incoming ether
  23. function () public payable { revert(); }
  24. }

我们对基本的Faucet示例做了很多改动。由于METFaucet将使用+METoken+中的+transferFrom+函数,它将需要两个额外的变量。其中一个将保存已部署的+METoken+合约地址。另一个将持有MET所有者的地址,他们将提供Faucet提款的批准。+METFaucet+将调用+METoken.transferFrom+并指示它将MET从所有者移至Faucet提取请求所来自的地址。

我们在这里声明这两个变量:

  1. StandardToken public METoken;
  2. address public METOwner;

由于我们的Faucet需要使用+METoken+和+METOwner+的正确地址进行初始化,因此我们需要声明一个自定义构造函数:

  1. // METFaucet constructor, provide the address of METoken contract and
  2. // the owner address we will be approved to transferFrom
  3. function METFaucet(address _METoken, address _METOwner) public {
  4. // Initialize the METoken from the address provided
  5. METoken = StandardToken(_METoken);
  6. METOwner = _METOwner;
  7. }

下一个改变是+withdraw+函数。METFaucet+使用+METoken+中的+transferFrom+函数,并要求+METoken+将MET传输给Faucet的接收者,而不是调用+transfer。

  1. // Use the transferFrom function of METoken
  2. METoken.transferFrom(METOwner, msg.sender, withdraw_amount);

最后,由于我们的Faucet不再发送ether,我们应该阻止任何人将ether送到+METFaucet+,因为我们不希望它被卡住。我们更改fallback函数以拒绝发进来的ether,使用+revert+功能还原任何收款:

  1. // REJECT any incoming ether
  2. function () public payable { revert(); }

现在我们的+METFaucet.sol+代码已准备就绪,我们需要修改migration脚本来部署它。这个migration脚本会有点复杂,因为+METFaucet+依赖于+METoken+的地址。我们将使用JavaScript promise按顺序部署这两个合约。创建+2_deply_contracts.js+,如下所示:

[[2_deploy_contracts]]

  1. var METoken = artifacts.require("METoken");
  2. var METFaucet = artifacts.require("METFaucet");
  3. var owner = web3.eth.accounts[0];
  4. module.exports = function(deployer) {
  5. // Deploy the METoken contract first
  6. deployer.deploy(METoken, {from: owner}).then(function() {
  7. // then deploy METFaucet and pass the address of METoken
  8. // and the address of the owner of all the MET who will approve METFaucet
  9. return deployer.deploy(METFaucet, METoken.address, owner);
  10. });
  11. }

现在,我们可以测试Truffle控制台中的所有内容。首先,我们使用+migrate+来部署合同。当+METoken+部署时,它会将所有MET分配给创建它的帐户,web3.eth.accounts[0]。然后,我们在METoken中调用+approve+函数来批准+METFaucet+代表+web3.eth.accounts[0]+发送1000 MET。最后,为了测试我们的Faucet,我们从+web3.eth.accounts[1]+调用+METFaucet.withdraw+并尝试提取10个MET。以下是控制台命令:

  1. $ truffle console --network ganache
  2. truffle(ganache)> migrate
  3. Using network 'ganache'.
  4. Running migration: 1_initial_migration.js
  5. Deploying Migrations...
  6. ... 0x79352b43e18cc46b023a779e9a0d16b30f127bfa40266c02f9871d63c26542c7
  7. Migrations: 0xaa588d3737b611bafd7bd713445b314bd453a5c8
  8. Saving artifacts...
  9. Running migration: 2_deploy_contracts.js
  10. Replacing METoken...
  11. ... 0xc42a57f22cddf95f6f8c19d794c8af3b2491f568b38b96fef15b13b6e8bfff21
  12. METoken: 0xf204a4ef082f5c04bb89f7d5e6568b796096735a
  13. Replacing METFaucet...
  14. ... 0xd9615cae2fa4f1e8a377de87f86162832cf4d31098779e6e00df1ae7f1b7f864
  15. METFaucet: 0x75c35c980c0d37ef46df04d31a140b65503c0eed
  16. Saving artifacts...
  17. truffle(ganache)> METoken.deployed().then(instance => { instance.approve(METFaucet.address, 100000) })
  18. truffle(ganache)> METoken.deployed().then(instance => { instance.balanceOf(web3.eth.accounts[1]).then(console.log) })
  19. truffle(ganache)> BigNumber { s: 1, e: 0, c: [ 0 ] }
  20. truffle(ganache)> METFaucet.deployed().then(instance => { instance.withdraw(1000, {from:web3.eth.accounts[1]}) } )
  21. truffle(ganache)> METoken.deployed().then(instance => { instance.balanceOf(web3.eth.accounts[1]).then(console.log) })
  22. truffle(ganache)> BigNumber { s: 1, e: 3, c: [ 1000 ] }

从结果中可以看出,我们可以使用 approve and transferFrom 工作流来授权一个合约转移另一个Token中定义的Token。如果使用得当,ERC20Token可以由EOA和其他合同使用。

但是,正确管理ERC20Token的负担会推送到用户界面。如果用户错误地尝试将ERC20Token转移到合同地址,并且该合同没有配备接收ERC20Token的功能,则Token将丢失。

ERC20Token的问题

ERC20Token标准的采用确实是爆炸性的。成千上万的Token已经启动,既可以尝试新的功能,也可以通过各种“众筹”拍卖和初始投币产品(ICO)筹集资金。然而,正如我们在将Token转移到合同地址的问题所看到的那样,存在一些潜在的陷阱。

ERC20Token不太明显的问题之一是它们暴露了Token和ether本身之间的细微差别。如果ether通过以接收者地址为目的地的交易转移,则Token转移发生在 specific Token contract state 中,并且将Token合同作为其目的地,而不是接收者的地址。Token合同跟踪余额并发布事件。在Token传输中,实际上没有交易发送给Token的接收者。相反,接收者的地址将被添加到Token合约本身的映射中。将ether发送到地址的交易会改变地址的状态。将Token转移到地址的交易只会改变Token合约的状态,而不会改变接收者地址的状态。即使是支持ERC20Token的钱包,也不会意识到Token的余额,除非用户明确将特定Token合约添加到“监视”中。一些钱包观察最受欢迎的Token合约,以检测由他们控制的地址持有的余额,但这仅限于ERC20合同的一小部分。

事实上,用户不太可能会追踪所有可能的ERC20Token合约中的所有余额。许多ERC20Token更像是垃圾邮件,而不是可用的Token。他们自动为拥有ether活动的帐户创建余额,以吸引用户。如果你有一个活动历史悠久的以太坊地址,特别是如果它是在预售中创建的,你会发现它充满了凭空出现的“垃圾”Tokens。当然,这个地址并不是真的充满了Token,而是那些Token合约有你的地址。如果你用于查看地址的资源管理器或钱包正在监视这些Token合约,才能看到这些余额。

Token不像ether。Ether通过+send+功能发送,并由合同中的任何payable函数或任何EOA接受。Token仅使用在ERC20合同中存在的+transfer+ 或+approve&transferFrom+函数发送,并且不会(至少在ERC20中)触发收款合同中的任何payable函数。Token的功能就像ether这样的加密货币,但它们带有一些细微的区别,可以打破这种错觉。

考虑另一个问题。要发送ether,或使用任何以太坊合同,你需要ether来支付gas。发送Token,你_也需要ether_。你无法用Token支付交易的gas,而Token合同也无法为你支付gas费用。这可能会导致一些相当奇怪的用户体验。例如,假设你使用交易所或Shapeshift将某些比特币转换为Token。你在钱包中“收到”该Token,该钱包会跟踪该Token的合同并显示你的余额。它看起来与你钱包中的任何其他加密货币相同。现在尝试发送Token,你的钱包会通知你,你需要ether才能这样做。你可能会感到困惑 - 毕竟你不需要ether接收Token。也许你没有ether。也许你甚至不知道该Token是以太坊上的ERC20Token,也许你认为这是一个拥有自己的区块链的加密货币。错觉就这样被打破了。

其中一些问题是ERC20Token特有的。其他更一般的问题涉及到以太坊内的抽象和界面边界。有些可以通过更改Token接口来解决,其他可能需要更改以太坊内的基础结构(例如EOAs和合同之间以及交易和消息之间的区别)。有些可能不完全“可解决”,并且可能需要用户界面设计来隐藏细微差别并使用户体验一致,而不管其底层区别如何。

在接下来的部分中,我们将看看试图解决其中一些问题的各种提案。

ERC223 - 一种建议的Token合同接口标准

ERC223提案试图通过检测目的地地址是否是合同来解决无意中将Token转移到合同(可能支持或不支持Token)的问题。ERC223要求用于接受Token的契约实现名为+TokenFallback+的函数。如果传输的目的地是合同并且合同不支持Token(即不实现+TokenFallback+),则传输失败。

为了检测目标地址是否为契约,ERC223参考实现使用了一小段内联字节码,并采用了一种颇具创造性的方式:

  1. function isContract(address _addr) private view returns (bool is_contract) {
  2. uint length;
  3. assembly {
  4. //retrieve the size of the code on target address, this needs assembly
  5. length := extcodesize(_addr)
  6. }
  7. return (length>0);
  8. }

你可以在这里看到有关ERC223提案的讨论:

https://github.com/ethereum/EIPs/issues/223

ERC223合同接口规范是:

  1. interface ERC223Token {
  2. uint public totalSupply;
  3. function balanceOf(address who) public view returns (uint);
  4. function name() public view returns (string _name);
  5. function symbol() public view returns (string _symbol);
  6. function decimals() public view returns (uint8 _decimals);
  7. function totalSupply() public view returns (uint256 _supply);
  8. function transfer(address to, uint value) public returns (bool ok);
  9. function transfer(address to, uint value, bytes data) public returns (bool ok);
  10. function transfer(address to, uint value, bytes data, string custom_fallback) public returns (bool ok);
  11. event Transfer(address indexed from, address indexed to, uint value, bytes indexed data);
  12. }

ERC223没有得到广泛的实施,ERC讨论中有一些关于向前兼容性和在合同接口级别或用户界面上实现更改之间的折衷的争论。争论仍在继续。

ERC777 - 一种建议的Token合同接口标准

另一项改进Token合同标准的提案是ERC777。该提案有几个目标,包括:

  • 提供ERC20兼容性界面

  • 使用+send+功能传输Token,类似于ether传输

  • 与ERC820兼容Token合同注册

  • 合同和地址可以通过在发送之前调用+TokensToSend+函数来控制它们发送的Token

  • 通过在接收者中调用+TokensReceived+函数来通知合同和地址

  • Token传输交易包含 userData 和 operatorData 字段中的元数据

  • 无论是发送到合同还是EOA,都以相同的方式运作

有关ERC777的详细信息和正在进行的讨论可以在这里找到:

https://github.com/ethereum/EIPs/issues/777

ERC777合同接口规范是:

  1. interface ERC777Token {
  2. function name() public constant returns (string);
  3. function symbol() public constant returns (string);
  4. function totalSupply() public constant returns (uint256);
  5. function granularity() public constant returns (uint256);
  6. function balanceOf(address owner) public constant returns (uint256);
  7. function send(address to, uint256 amount) public;
  8. function send(address to, uint256 amount, bytes userData) public;
  9. function authorizeOperator(address operator) public;
  10. function revokeOperator(address operator) public;
  11. function isOperatorFor(address operator, address TokenHolder) public constant returns (bool);
  12. function operatorSend(address from, address to, uint256 amount, bytes userData, bytes operatorData) public;
  13. event Sent(address indexed operator, address indexed from, address indexed to, uint256 amount, bytes userData, bytes operatorData);
  14. event Minted(address indexed operator, address indexed to, uint256 amount, bytes operatorData);
  15. event Burned(address indexed operator, address indexed from, uint256 amount, bytes userData, bytes operatorData);
  16. event AuthorizedOperator(address indexed operator, address indexed TokenHolder);
  17. event RevokedOperator(address indexed operator, address indexed TokenHolder);
  18. }

ERC777的参考实现与提案相关联。ERC777依赖于ERC820中关于注册合同的并行提案。关于ERC777的一些争论是关于同时采用两个大变化的复杂性:一个新的Token标准和一个注册标准。讨论仍在继续。

ERC721 - 不可替代的Token(契据)标准

我们目前看到的所有Token标准都是_可互换_Token,这意味着Token的每个单元都是完全可以互换的。ERC20Token标准仅跟踪每个帐户的最终余额,并且(明确地)跟踪任何Token的出处。

ERC721提案是_不可互换的_ Tokens标准,也称为_契据_ deeds

牛津词典:

  1. 契约:签署和交付的法律文件,尤其是关于财产或合法权利所有权的法律文件。

契约一词的使用旨在反映“财产所有权”部分,即使这些部分在任何司法管辖区都不被承认为“法律文件”,至少目前不是。

不可互换的Token追踪独特事物的所有权。拥有的东西可以是数字项目,例如游戏物品或数字收藏品。或者,这种东西可以是物理事物,其物主通过Token进行跟踪,例如房屋,汽车,艺术品等。契约也可以代表负值的东西,例如贷款(债务),留置权,地役权等。ERC721标准对所有权由契约追踪的事物的性质没有限制或期望,只是它可以是唯一标识,在这个标准的情况下是由256位标识符实现的。

标准和讨论的细节在两个不同的GitHub位置进行跟踪:

初步建议: https://github.com/ethereum/EIPs/issues/721

继续讨论: https://github.com/ethereum/EIPs/pull/841

要掌握ERC20和ERC721之间的基本差异,只需查看ERC721中使用的内部数据结构即可:

  1. // Mapping from deed ID to owner
  2. mapping (uint256 => address) private deedOwner;

ERC20跟踪属于每个所有者的余额,所有者是映射的主键,ERC721跟踪每个契约ID以及谁拥有它,契约ID是映射的主键。从这个基本差异衍生出不可替代的Token的所有属性。

ERC721 合同接口规范是:

  1. interface ERC721 /* is ERC165 */ {
  2. event Transfer(address indexed _from, address indexed _to, uint256 _deedId);
  3. event Approval(address indexed _owner, address indexed _approved, uint256 _deedId);
  4. event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
  5. function balanceOf(address _owner) external view returns (uint256 _balance);
  6. function ownerOf(uint256 _deedId) external view returns (address _owner);
  7. function transfer(address _to, uint256 _deedId) external payable;
  8. function transferFrom(address _from, address _to, uint256 _deedId) external payable;
  9. function approve(address _approved, uint256 _deedId) external payable;
  10. function setApprovalForAll(address _operateor, boolean _approved) payable;
  11. function supportsInterface(bytes4 interfaceID) external view returns (bool);
  12. }

ERC721还支持两个_*可选*_接口,一个用于元数据,一个用于枚举契约和所有者。

用于元数据的ERC721可选接口是:

  1. interface ERC721Metadata /* is ERC721 */ {
  2. function name() external pure returns (string _name);
  3. function symbol() external pure returns (string _symbol);
  4. function deedUri(uint256 _deedId) external view returns (string _deedUri);
  5. }

用于枚举的ERC721可选接口是:

  1. interface ERC721Enumerable /* is ERC721 */ {
  2. function totalSupply() external view returns (uint256 _count);
  3. function deedByIndex(uint256 _index) external view returns (uint256 _deedId);
  4. function countOfOwners() external view returns (uint256 _count);
  5. function ownerByIndex(uint256 _index) external view returns (address _owner);
  6. function deedOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256 _deedId);
  7. }