ERC系列协议标准
ERC系列协议标准
(一)概述
在以太坊中,发布智能合约是自由的。但是,很多第三方想要做的事情可能有所类似,故延伸出来了许多基于以太坊的ERC系列协议;
基于以太坊的区块链平台的数字资产分为原生令牌(硬币)和代币(令牌)两大类,原生的自然就是以太坊(ETH),令牌是通过智能合约创设的数字资产。代币则是按ERC创设协议分类,主流类别有ERC20代币,ERC223代币,ERC721代币,另外还有一些小众类别,包括ERC621,ERC721和ERC827等。
ERC代表“Etuereum征求意见”,这是Ethereum版的意见征求稿(RFC),ERC是由以太坊开发者为以太坊社区编写,ERC后面的数字是议案的编号。当开发人员提交了一个以太坊改进方案(EIP),该方案一旦EIP被委员会批准并最终确定后,一个新的以太坊开发标准就形成了,它包括协议规范和合约标准;【注:好多新人都不知道这个.ERC = 以太坊请求评论,你要看EIP这个项目,应该去看问题.ERC20,就是问题#20,ERC721,就是问题#721。并不是所有的ERC都是标准,最终被采纳的问题才会被收进EIP ; EIP的readme里边有目前的状态网站链接https://eips.ethereum.org】具体表现可以用抽象合约表现出来【注意:ERC并不是指某些特定的合约,而是指合约应该符合某些规范】;根据具备某些特征归类目前有ERC20,ERC223,ERC664,ERC667,ERC721,ERC875,ERC1155及ERC156等等合约协议规
(二)例子
1.ERC20
范;下面先从最常用的ERC20铸币合约标准开始说,以下为一个标准的ERC20的解析:
pragma solidity ^0.4.16; /** ERC20标准 */ contract ERC20 { /** 代币的名字,例如"Gavin token" */ function name() constant public returns (string name); /** 代币的简称,例如:GAVC 也是我们一般在代币交易所看到的名字 */ function symbol() public constant returns (string symbol); /** 代币的最小分割量 token使用的小数点后几位。比如如果设置为3,就是支持0.001表示 */ function decimals() public constant returns (uint8 decimals); /** token的总量 */ function totalSupply() public constant returns (uint totalSupply); /** 【余额】 返回某个地址(账户)的账户余额 */ function balanceOf(address _owner) public constant returns (uint balance); /** 【转账】 交易代币 从消息发送者账户中往_to账户转数量为_value的token 从代币合约的调用者地址上转移 _value的数量token到的地址 _to,并且必须触发Transfer事件 */ function transfer(address _to, uint _value) public returns (bool success); /** 【替某人个别人转账】 从账户_from中往账户_to转数量为_value的token,与approve方法配合使用 从地址 _from发送数量为 _value的token到地址 _to,必须触发Transfer事件。 transferFrom方法用于允许合约代理某人转移token。条件是from账户必须经过了approve。这个后面会举例说明 */ function transferFrom(address _from, address _to, uint _value) public returns (bool success); /** 【允许量值】 限定_spender能从合约调用账户中转出数量为_value的token */ function approve(address _spender, uint _value) public returns (bool success); /** 【限额】 获取账户_spender可以从账户_owner中转出token的数量 */ function allowance(address _owner, address _spender) public constant returns (uint remaining); /** 发生转账时必须要触发的事件 一般位于 transfer 和 transferFrom 函数中 */ event Transfer(address indexed _from, address indexed _to, uint _value); /** 当函数approve(address _spender, uint256 _value)成功执行时必须触发的事件 */ event Approval(address indexed _owner, address indexed _spender, uint _value); }
上述就是ERC20 标准接口总共分为三大类: 常量、功能函数及事件
【常量】
<1> name: 代币名称,就是一串指定的字符串,如: "GavinNetToken"
<2> symbol: 代币的代号,由3-4个大写字母组成, 如: "GVTC"
<3> decimals: 小数点后几位,表示该代币支持最小的小数位是多少,默认为18 (ETH也是18,之所以需要有小数位字段是因为EVM 不支持小数点运算,需要在做计算的时候先转成整型,最后根据小数位把运算结果转换会对应的小数位)
【功能函数】
<4> totalSupply(): 总供应量;表示该代币的总发行量 [后期并没有承诺不会被更改]
<5> balanceOf(address _owner): 查询余额;根据账户地址查询该地址的余额,为无符号整数常量
<6> transfer(address _to, uint _value): 转账;表示合约的调用者 转移_value 数量的token至 _to 目标账户地址
<7> transferFrom(address _from, address _to, uint _value): 替别人转账;合约调用者代替_from给_to转账 _value数目,【该方法需要有approve()方法的先决条件,即是说 需要_from 先用approve() 给当前合约的调用者 设置课允许当前合约调用者 代替自己(即: _from)给_to转账_value数目】
<8> approve(address _spender, uint _value): 给某人赋予多少转账数目权限;当前合约调用者给予_spender账户有替代自己给别人转账 总额为_value数目 【注意,这个方法会对一个全局的 map[最终花钱者(approve()方法的调用者)][替代自己拿着自己的钱去花者] 进行设置,后续transferFrom函数每次转账后都需要更新该Map中记载的被给予转账的余额;allowance()方法会查询这个map中的值】
<9> allowance(address _owner, address _spender): 查询限额的数目;查询_spender还可以从_owner拿多少数目去花销【底层读取的是 approve()方法中设置的那个全局的map的值】
【事件】
<10> event Transfer : 每次调用 function transfer时最末尾都需要调用的事件,用于捕获交易的日志
<11> event Approval : 每次调用 function approve时最末尾都需要调用的事件,用于捕获给予第三方代替自己做交易事件的日志
至此ERC20标准的各项方法说明已经写完;下面我们来看看ERC223标准:
各位币圈大佬都知道,现有的 ETH 代币基本上都基于 ERC20 标准实现,而 ERC20 存在某些严重问题【ERC-20标准还有待完善。其中一个障碍是,将令牌直接发送给令牌的智能合同将导致资金损失】
根据 ERC20 标准,交易(transaction)可以有两种处理方式:通过transfer函数和通过approve+transferFrom,
对于开发来说,事件处理(Event Handling)是一个标准性的操作,我们所说的交易也可以被当做一种事件进行考虑。
但是很遗憾,在 ERC20 标准中,缺乏事件处理机制,也就是说,当交易发生的时候,收款人并不会得知这一消息。
在 ERC20 标准中,我们要求用户向合约发送代币的时候,必须使用approve+transferFrom模式进行代币转账,与此同时,如果收件人是一个账户地址,就必须使用transfer函数完成交易。如果用户搞错了,这些代币就会由于合约无法识别交易而被困在合约中。而合约没有提供一个可以把这些困住的代币提取出来的方法。
到目前为止,大概有 $3,000,000 的损失已经发生了;
2.ERC223
ERC223 是 ERC20 的一个升级版本。ERC223 标准提供了更安全的方法用于完成交易。
在 ERC223 标准中,交易发起方无需关注接收方到底是合约还是钱包地址,使代币交易更接近 ETH 交易。
相对比 ERC20 标准,ERC223 通过全新的transfer函数,自动回滚向不支持代币的合约发送代币的行为。
它的实现方法是定义一个接收器(receiver),接受方合约必须通过它来正确地处理收到的代币,否则就会有异常抛出。
就好像你在发送 ETH 到一个没有payable关键字的合约一样;其实就是 【它与之前的ERC20相比,该标准更注重保护合约本身和防止您的数字代币丢失。作为ERC20的升级方案,该标准有不允许代币转到不支持代币接收和处理的合约中的功能;ERC223标准允许用户发送代币到钱包或合同地址,从而消除了丢失代币的危险性。同时ERC223中的转让合同功能让其合同的gas消耗比ERC20少。简而言之,ERC223更侧重于安全,被誉为取代ERC20的标准】,下面我们先看看具体的ERC223标准 首先我们需要有2个*.sol文件;先看看
ERC223_Interface.sol
文件的内容
contract ERC223 { uint public totalSupply; /** 代币名称 【同ERC20】 */ function name() public constant returns (string _name); /** 代币的标识符 【同ERC20】 */ function symbol() public constant returns (string _symbol); /** 小数点后几位 【同ERC20】 */ function decimals() public constant returns (uint8 _decimals); /** 代币总发行量 【同ERC20】 */ function totalSupply() public constant returns (uint256 _supply); /** 查询某地址余额 【同ERC20】 */ function balanceOf(address who) public constant returns (uint); /** 转账给 某个外部账户 【同ERC20】 */ function transfer(address to, uint value) public returns (bool ok); /** 转账给合约 该合约必须是实现了 名为: tokenFallback的函数 data可以附加到这个令牌交易中,它将永远保持在块状(需要更多的gas)。 _data可以是空的 */ function transfer(address to, uint value, bytes data) public returns (bool ok); /** 重载 转账给合约 */ function transfer(address to, uint value, bytes data, string custom_fallback) public returns (bool ok); event Transfer(address indexed from, address indexed to, uint value, bytes indexed data); }
另外
Receiver_Interface.sol
文件的内容为:
pragma solidity ^0.4.16; /* * Contract that is working with ERC223 tokens */ contract ContractReceiver { struct TKN { address sender; //调用合约的人 uint value; bytes data; bytes4 sig; //签名 }
/**
该函数表明了当调用了 transfer交易函数由A(可以使任何账户) 向合约B交易token时时,函数中的逻辑必须是
_from是令牌发送者,_value是传入令牌的数量,
_data是附加的数据,类似于Ether事务中的数据。 适用于以太交易的回退功能,并且不返回任何内容。
【注意】: 过滤哪些令牌(通过token合约的账户地址)发送可以被发送很重要。
令牌发送者(谁发起了代币交易的人)将_from传入tokenFallback函数内。
【重要】: 这个函数必须命名为tokenFallback,并使用参数地址uint256,字节来匹配函数签名0xc0ee0b8a
*/
function tokenFallback(address _from, uint _value, bytes _data) public {
TKN memory tkn;
tkn.sender = _from;
tkn.value = _value;
tkn.data = _data;
uint32 u = uint32(_data[3]) + (uint32(_data[2]) << 8) + (uint32(_data[1]) << 16) + (uint32(_data[0]) << 24);
tkn.sig = bytes4(u);
/* tkn变量是Ether交易的msg变量的模拟 * tkn.sender是发起这个令牌交易的人(类似于msg.sender) * tkn.value发送的令牌数(msg.value的类比) * tkn.data是令牌交易的数据(类似于msg.data) * tkn.sig是4字节的功能签名 */
}
}
【注意:】将在接收方合约中调用的token备用功能的函数必须命名为tokenFallback,并使用参数address, uint256,bytes。 此函数必须具有0xc0ee0b8a签名[bytes4(keccak256('tonken(address,uint256,bytes)')) == c0ee0b8a ??]
然后,当我们需要来说一说怎么用这两个文件【这些代码都是网上抄的,原作者估计也是翻译的 注释写的有些不对】下面演示一个ERC223的token代币合约示例
ERC223_Token.sol
文件内容:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
pragma solidity ^0.4.16; import "./Receiver_Interface.sol"; import "./ERC223_Interface.sol"; /** * ERC223 token by Dexaran * * https://github.com/Dexaran/ERC223-token-standard */ /* https://github.com/LykkeCity/EthereumApiDotNetCore/blob/master/src/ContractBuilder/contracts/token/SafeMath.sol */ contract SafeMath { uint256 constant public MAX_UINT256 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; function safeAdd(uint256 x, uint256 y) constant internal returns (uint256 z) { if (x > MAX_UINT256 - y) revert(); return x + y; } function safeSub(uint256 x, uint256 y) constant internal returns (uint256 z) { if (x < y) revert(); return x - y; } function safeMul(uint256 x, uint256 y) constant internal returns (uint256 z) { if (y == 0) return 0; if (x > MAX_UINT256 / y) revert(); return x * y; } } /** ERC223示例的智能合约代码 */ contract ERC223Token is ERC223, SafeMath { mapping(address => uint) balances; string public name; string public symbol; uint8 public decimals; uint256 public totalSupply; // 获取token的名称 function name() public view returns (string) { return name; } // 获取token的符号 function symbol() public view returns (string) { return symbol; } // 获取token精确到小数点后的位数 function decimals() public view returns (uint8) { return decimals; } // 获取token的发布总量 function totalSupply() public view returns (uint256) { return totalSupply; } // 当用户或其他合同想要转移资金时调用的功能。 function transfer(address _to, uint _value, bytes _data, string _custom_fallback) public returns (bool success) { //如果to是合约 if(isContract(_to)) { if (balanceOf(msg.sender) < _value) revert(); //如果当前的余额不够就抛出 balances[msg.sender] = safeSub(balanceOf(msg.sender), _value);//发送者的余额做减法 balances[_to] = safeAdd(balanceOf(_to), _value); //接收者的余额做加法 ContractReceiver receiver = ContractReceiver(_to); //初始化接收合约,构造函数参数为接收者的合约地址 receiver.call.value(0)(bytes4(sha3(_custom_fallback)), msg.sender, _value, _data); Transfer(msg.sender, _to, _value, _data); return true; } else { return transferToAddress(_to, _value, _data); } } // 当用户或其他合同想要转移资金时调用的功能。 function transfer(address _to, uint _value, bytes _data) public returns (bool success) { if(isContract(_to)) { return transferToContract(_to, _value, _data); } else { return transferToAddress(_to, _value, _data); } } // 类似于ERC20传输的标准功能传输,没有_data。 // 由于向后兼容性原因而增加。 function transfer(address _to, uint _value) public returns (bool success) { //类似于没有_data的ERC20传输的标准功能传输 //由于向后兼容性原因而增加 bytes memory empty; if(isContract(_to)) {//如果是合约 return transferToContract(_to, _value, empty); } else { return transferToAddress(_to, _value, empty); } } /** 判断某账户地址是EOA地址还是合约地址 组装定地址字节码。 如果存在字节码,那么_addr是一个合约。 */ function isContract(address _addr) private returns (bool is_contract) { uint length; Assembly { //检索目标地址上的代码大小,这需要汇编 length := extcodesize(_addr) } return (length>0); } /** 当传递目标是一个地址【即: 外部账户】时调用函数 */ function transferToAddress(address _to, uint _value, bytes _data) private returns (bool success) { if (balanceOf(msg.sender) < _value) revert(); balances[msg.sender] = safeSub(balanceOf(msg.sender), _value); balances[_to] = safeAdd(balanceOf(_to), _value); Transfer(msg.sender, _to, _value, _data); return true; } /** 当传递目标是一个合约【合约账户】时调用函数 【注意】token接收者是合约的话,需要调用 自定义的具备了 tokenFallback函数的 合约对象的tokenFallback函数 【要求接受token的合约必须具备了tokenFallback函数】 */ function transferToContract(address _to, uint _value, bytes _data) private returns (bool success) { if (balanceOf(msg.sender) < _value) revert(); balances[msg.sender] = safeSub(balanceOf(msg.sender), _value); balances[_to] = safeAdd(balanceOf(_to), _value); ContractReceiver receiver = ContractReceiver(_to); receiver.tokenFallback(msg.sender, _value, _data); //必须要调用这个回调 Transfer(msg.sender, _to, _value, _data); return true; } //得到_owner的余额 function balanceOf(address _owner) public constant returns (uint balance) { return balances[_owner]; } }
以上ERC223标准的token发币合约示例表示,当需要调用交易函数把ERC223标准token交易给某个合约地址时,要求合约必须具备tokenFallback函数【合约开发人员希望他们的合约使用指定的token,那么合约开发人员必须实现tokenFallback,并使用参数address,uint256,bytes。 此函数必须具有0xc0ee0b8a签名】,否则转账失败;现有的代币肯定是大部分基于 ERC20 标准的,迁移难度很大,毕竟这个事情需要大家一起动。至于新的代币,很多已经开始使用 ERC223 标准了,目前已经超过了三万种(某位网上的博主说的,不是我说的。。。)
以上介绍了两种可以发行 【可代替性通证】的合约,(可代替性就是可以代替所有没有唯一性的事物,如:钱、积分等);
3.ERC721
下面开始研究针对【不可代替性的事物】的一些合约标准,【CryptoKitties 以太猫】相信大家都对这款曾经让以太坊网络一度瘫痪的 猫科动物繁殖类无聊烧钱游戏有一点点耳闻吧?下面是代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
pragma solidity ^0.4.16; /** 以太猫使用的ERC721的合约标准 */ contract ERC721 { // Required method function totalSupply() public constant returns (uint256 totalSupply); function balanceOf(address _owner) public constant returns (uint256 balance); /** 根据代币Id查询代币持有者 */ function ownerOf(uint256 _tokenId) public constant returns (address _owner); function transfer(address _to, uint256 _tokenId) public; function approve(address _to, uint256 _tokenId) public; /** 在Approve 允许的条件下, 交易方msg.sender 调用该函数可以指定_tokenId的代币从他人那转至自己名下 类似于ERC20的 transferFrom */ function takeOwnership(uint256 _tokenId) public; // Optional method function name() public constant returns (string name); function symbol() public constant returns (string symbol); /** 根据持有者和索引查询所持有的代币 */ function tokenOfOwnerByIndex(address _owner, uint256 _index) public constant returns (uint tokenId); /** 查看代币的元数据 根据代币Id查询到元数据对应的URL, 里面包含了 代币名称,图像和描述等 */ function tokenMetadata(uint256 _tokenId) public constant returns (string infoUrl); // Events event Transfer(address indexed _from, address indexed _to, uint256 _tokenId); event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId); }
可以看到ERC721协议其实和ERC20协议还是很类似的,代币名称、代币符号、代币支持的小数位等等,然后在这基础上做了点改动,因为在ERC721中每个代币都将是唯一的一串Hash码,且代币是可以在各个持有者间相互转让的;所以有具备查询代币持有者地址的ownerOf函数、有根据持有者及索引查询持有的某个代币的tokenOfOwnerByIndex函数等等以上在代码里面都有所说明,这里就不做赘述;
4.ERC1155
首先,我们要说下不管是ERC20系【可替代性通证】还是ERC721系【不可替代性通证】的token都具备的一个问题,对于一个项目中如果有几万种完全不同性质的东西,比如: 钱是钱、积分是积分、收藏品是收藏品那么我们需要部署几万种token的合约【不管是ERC20的还是ERC721的】那么问题来了,首先这几万种合约里面必然有大量的重复代码,但是矿工们依然会把这几万种合约都上链,【注意我说的是在一个项目里面,比如: 游戏项目】,所以给每一个不同的物品设立一个独立的合约,这产生的巨大成本和之后带来的管理费用,都将是几乎让人无法承受的,那么ERC1155就是为了解决这种问题而生的,现在『物品』(可能包含ERC20的token或ERC721的token或两者都有)可以被单一的一个合约(打包处理)来定义了。合约里包含区别token们所需的最小量的数据。好比,后来出现的视频压缩技术,后一帧只记录了与前一帧的不同之处,所以极大的压缩了整个视频的体积,合约的状态包含了每个token ID的配置信息和管理收集的所有行为。ERC-1155的灵活性更强,它使得开发者可以自行选择是批量生成某一种特定的token,还是构建不可被复制的惟一元数据;说白点就是说ERC1155可以把一种token换成为另外一种token,最大进步就是可以融合不同token(可能是『可替换』的代币与『不可替换』的代币的混合体)进行『打包处理』;下面是一个ERC1155的合约标准
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
pragma solidity ^0.4.16; /** 针对 可替代token的 https://github.com/ethereum/EIPs/issues/1155 */ contract ERC1155 { // Events event Approval(address indexed _owner, address indexed _spender, uint256 indexed _itemId, uint256 _value); event Transfer(address indexed _from, address indexed _to, uint256 indexed _itemId, uint256 _value); // Required Functions /** 【两个方法一个处理单个的一个批量处理】 【请参照 ERC20的transfer函数】 将每个itemId[]的数量转移到指定的地址。 每个参数数组应该是相同的长度,每个索引都是相关的。 必须触发 Transfer事件 itemId 可以看做是某个特定的tokenId */ function transfer(address _to, uint256 _itemId, uint256 _value) external; function batchTransfer(address _to, uint256[] _itemIds, uint256[] _values) external; /** 【两个方法一个处理单个的一个批量处理】 【请参照 ERC20的transferFrom函数】 将每一个itemId[]的数量从一个或多个地址转移到指定地址。 每个参数数组应该是相同的长度,每个索引都是相关的 必须触发 Transfer事件 itemId 可以看做是某个特定的tokenId */ function transferFrom(address _from, address _to, uint256 _itemId, uint256 _value) external; function batchTransferFrom(address _from, address _to, uint256[] _itemIds, uint256[] _values) external; /** 【两个方法一个处理单个的一个批量处理】 【请参照 ERC20的approve函数】 批准一个帐户可以替代表另一个帐户(使用转账)转移最大数量的多个itemId数目 必须触发 Approval事件 */ function approve(address _spender, uint256 _itemId, uint256 _value) external; function batchApprove(address _spender, uint256[] _itemIds, uint256[] _values) external; /** 增加一个或多个itemId的余量,而不需要重置为0 【及变更approve函数指定的允许转移量??】 必须触发 Approval事件 */ function increaseApproval(address _spender, uint256 _itemId, uint256 _addedValue) external; function batchIncreaseApproval(address _spender, uint256[] _itemIds, uint256[] _addedValues) external; /** 减少一个或多个itemId的余量,而不需要重置为0 必须触发 Approval事件 */ function decreaseApproval(address _spender, uint256 _itemId, uint256 _subtractedValue) external; function batchDecreaseApproval(address _spender, uint256[] _itemIds, uint256[] _subtractedValues) external; // Required View Functions function totalSupply(uint256 _itemId) external view returns (uint256); /** 根据对应的tokenId查看余额 */ function balanceOf(uint256 _itemId, address _owner) external view returns (uint256); /** 根据对应的tokenId及拥有者及代替拥有者花钱者(注意 钱还是话拥有者的,只是拥有者给予某些权力给 第三者可以挪用她的部分资金) 具体参照ERC20 自明 */ function allowance(uint256 _itemId, address _owner, address _spender) external view returns (uint256); // Optional View Functions function name(uint256 _itemId) external view returns (string); function symbol(uint256 _itemId) external view returns (string); function decimals(uint256 _itemId) external view returns (uint8); } /** 针对不可替代token的拓展 */ contract ERC1155NonFungible { // Optional Functions for Non-Fungible Items /** For NFTs, this returns the owner of a specific _itemId. 在【 Non-Fungible Items】,返回当前tokenId 所属者 【参照ERC721】 */ function ownerOf(uint256 _itemId) external view returns (address); /** 返回当前tokenId的描述的URL 【参照ERC721】 */ function itemURI(uint256 _itemId) external view returns (string); /** 列举可用的Non-Fungible Items */ function itemByIndex(uint256 _itemId, uint128 _index) external view returns (uint256); /** 列举分配给所有者的Non-Fungible Items */ function itemOfOwnerByIndex(uint256 _itemId, address _owner, uint128 _index) external view returns (uint256); }
基本上就是ERC1155的注释说明。
5.ERC165
自己看注释就知道ERC165是干什么的了,顺便说一下CryptoKities合约里面有实现了ERC165的。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/** 这个才是 ERC165 标准 就是一个接口 里面就一个方法 创建标准方法以发布和检测智能合约实现的接口 ERC165同样是一个合约标准,这个标准要求合约提供其实现了哪些接口, 这样再与合约进行交互的时候可以先调用此接口进行查询。 interfaceID为函数选择器,计算方式有两种, 如:bytes4(keccak256('supportsInterface(bytes4)')); 或ERC165.supportsInterface.selector,多个函数的接口ID为函数选择器的异或值 此功能必须返回一个bool并使用最多30,000gas */ interface ERC165 { /// @notice Query if a contract implements an interface /// @param interfaceID The interface identifier, as specified in ERC-165 /// @dev Interface identification is specified in ERC-165. This function /// uses less than 30,000 gas. /// @return 'true' if the contract implements 'interfaceID' and /// 'interfaceID' is not 0xffffffff, 'false' otherwise function supportsInterface(bytes4 interfaceID) external view returns (bool); }
OK〜 因为这一些列的问题实在是太多了如果想看,请自行去github 中里Ethereum的EIPs里面的issue里面去看吧,(注:想看什么标准就看问是否有,我们可以这样看https://github.com/ethereum/EIPs/issues/XXX,其中XXX就是ERCXXX里面的数字也就是ERC20。
例如:看https://github.com/ethereum/EIPs/issues/20