主页 > 下载imtoken被盗 > 教程 | 以太坊开发演练,第 4 部分:代币和 ERC
教程 | 以太坊开发演练,第 4 部分:代币和 ERC
以太坊开发攻略系列:
从开发者的角度来看,以太坊代币只不过是智能合约。 如果把它比作饮料,就好比任何人都可以用自己的方式做出适合自己口味的咖啡。
您可能还听说过 ERC20、ERC721 或其他标准。 这些是开发者社区所恪守的基本功能。 在此基础上,每个人都可以使用自己的开发功能,按照自己的方式创建脚本来管理虚拟货币。
加勒比海盗中有一句台词很好地捕捉到了这种情况:
-代码更像是“指南”而不是实际规则-
从长远来看,遵循标准有许多不容忽视的好处。 首先,如果一个代币是按照一定的标准生成的,那么大家就会知道这个代币的底层功能,知道如何与之交互,这样就会有更多的信任。 Mist 等去中心化程序(DApp)可以直接识别其代币的特征,并通过特定的 UI 进行处理。 此外,社区还开发了代币智能合约的标准实现,它使用类似于 OpenZeppelin 的架构。 此实现方式已被众多高手验证,可作为token开发的起点。
本文将从头开始提供一个不完整但符合 ERC20 标准的基本代币实现,然后将其转换为符合 ERC721 标准的实现。 这使读者可以看到两个标准之间的区别。
写这篇文章的出发点是希望大家了解代币是如何运作的,过程不是黑箱; 另外,对于ERC20标准来说,虽然已经被广泛接受了至少两年,但是如果单纯的从标准框架中生成自己的Token,也存在一定的不易发现的失败点。
生成自己的令牌
ERC20 是 Fungible 代币的标准,可以被其他应用程序(从钱包到去中心化交易所)重复使用。 同质化是指同一类型的代币可以互换以太坊教程,换句话说,所有的代币都是等价的(就像硬币一样,某种美元和其他美元没有区别)。 而一个不可替代的代币(Non-fungible Token)代表了一个特定的价值(比如住房、财产、艺术品等)。 同质代币有其内在价值,而非同质代币只是价值智能合约的代表。
提供符合ERC20标准的代币,需要实现以下功能和事件:
contract ERC20Interface { function totalSupply() public constant returns (uint); function balanceOf(address tokenOwner) public constant returns (uint balance); function allowance(address tokenOwner, address spender) public constant returns (uint remaining); function transfer(address to, uint tokens) public returns (bool success); function approve(address spender, uint tokens) public returns (bool success); function transferFrom(address from, address to, uint tokens) public returns (bool success); event Transfer(address indexed from, address indexed to, uint tokens); event Approval(address indexed tokenOwner, address indexed spender, uint tokens); }
该标准不提供功能的实现,因为您可以按照自己喜欢的方式编写任何代码。 如果不需要提供一些功能,只需要按照标准返回null/false的值即可。
注意:本文对代码不怎么强调。 你只需要了解内部机制以太坊教程,所有代码将在文末链接。
完成
首先,您需要给令牌一个名称,以便使用公共变量':
string public name = "Our Tutorial Coin";
然后给token一个token:
string public symbol = "OTC";
当然,必须有具体的小数位数:
uint8 public decimals = 2;
由于 Solidity 不完全支持浮点数,因此所有数字都必须表示为整数。 例如,对于一个数字“123456”,如果使用2位小数,则表示“1234.56”; 如果使用 4 位小数,则表示“12.3456”。 0 十进制表示令牌是不可分割的。 以太坊的加密货币 Ether 使用 18 位小数。
一般来说,代币不需要使用 18 位小数,因为它们有神圣的以太支持(除非你想被其他专家指责为什么使用这个神圣的数字)。
您需要计算总共发行了多少代币,并跟踪每个人拥有的代币数量:
uint256 public totalSupply; mapping(address => uint256) balances;
当然,你需要从 0 个代币开始,除非有一些是在创建代币智能合约时生成的,如下例所示:
// The constructor function of our Token smart contract function TutoCoin() public { // We create 100 tokens (With 2 decimals, in reality it's 1.00 token) totalSupply = 100; // We give all the token to the msg.sender (in this case, it's the creator of the contract) balances[msg.sender] = 100; // With coins, don't forget to keep track of who has how much in the smart contract, or they'll be "lost". }
totalsupply() 函数只是从 totalSupply 变量中获取值:
function totalSupply() public constant returns (uint256 _totalSupply) { return totalSupply; }
balanceOf() 也类似:
// Gets the balance of the specified address.
function balanceOf(address tokenOwner) public view returns (uint256 balance) {
return balances[tokenOwner];
}
接下来是ERC20的神奇之处,transfer()函数是一个将代币从一个地址发送到另一个地址的函数:
function transfer(address _to, uint256 _value) public returns (bool) { // avoid sending tokens to the 0x0 address require(_to != address(0)); // make sure the sender has enough tokens require(_value <= balances[msg.sender]); // we substract the tokens from the sender's balance balances[msg.sender] = balances[msg.sender] - _value; // then add them to the receiver balances[_to] = balances[_to] + _value; // We trigger an event, note that Transfer have a capital "T", it's not the function itself with a lowercase "t" Transfer(msg.sender, _to, _value); // the transfer was successfull, we return a true return true; }
以上基本上就是ERC20代币标准的核心内容。
approve()、transferFrom() 和 allowance() 是使代币符合 ERC20 标准的函数,但它们很容易受到攻击。
当源地址通过approve()函数授权另一个地址时,授权地址可以使用transferFrom()函数花费源地址中的token。 allowance() 只是一个从其他地址获取可用信用的函数。
这些功能存在安全隐患,因为当源地址对授权地址进行授信并可以花费X个代币时,出现意外原因将授信更改为Y个代币(增加或减少金额),授权地址很可能X代币在执行重信前转移; 当Y代币的新增授信完成后,即可转出Y代币。 在之前的系列文章中,我提到过交易在挖矿过程中,无法确定状态,矿工可以在交易挖矿过程中控制执行时间。
针对ERC20中的一些其他问题,发布了更加安全和容错的transferFrom()实现等解决方案(前面提到,标准只是一些函数原型和行为定义,具体细节由开发者自己),并正在讨论中实施,包括 ERC223 和 ERC777。
ERC223 方案的动机是避免将代币发送到错误的地址或不支持此类代币的合约。 由于上述原因损失了数千美元。 此需求记录为以太坊案例后续开发功能的第223篇。 在支持其他功能的同时,ERC777标准对接收地址有“代币即将接收”的提醒功能。 ERC777 方案似乎有可能取代 ERC20。
ERC721
目前,ERC721 与 ERC20 及其近亲有本质区别。
在 ERC721 中,令牌是唯一的。 ERC721于数月前被提出,使用ERC721标准实现的虚拟猫游戏合集CryptoKitties备受关注。
Ethercat 游戏实际上是智能合约中的不可替代代币,在游戏中用猫的形象来表示。
如果我们想将 ERC20 合约转换为 ERC721 合约,我们需要知道 ERC721 是如何跟踪代币的。
在 ERC20 中,每个地址都有一个账本表,而在 ERC721 合约中,每个地址都有一个代币列表:
mapping(address => uint[]) internal listOfOwnerTokens;
由于Solidity本身的限制,它不支持对队列的indexOF()操作,所以我们不得不手动跟踪队列令牌:
mapping(uint => uint) internal tokenIndexInOwnerArray;
当然,你可以使用自己的代码库来查找元素的索引。 考虑到索引时间可能会很长,最好的做法是使用映射的方式。
为了更容易追踪代币,您还可以为代币所有者设置一个映射表:
mapping(uint => address) internal tokenIdToOwner;
以上是两种标准最大的区别。 ERC721 中的 transfer() 函数将为令牌设置一个新的所有者:
function transfer(address _to, uint _tokenId) public (_tokenId) { // we make sure the token exists require(tokenIdToOwner[_tokenId] != address(0)); // the sender owns the token require(tokenIdToOwner[_tokenId] == msg.sender); // avoid sending it to a 0x0 require(_to != address(0)); // we remove the token from last owner list uint length = listOfOwnerTokens[msg.sender].length; // length of owner tokens uint index = tokenIndexInOwnerArray[_tokenId]; // index of token in owner array uint swapToken = listOfOwnerTokens[msg.sender][length - 1]; // last token in array listOfOwnerTokens[msg.sender][index] = swapToken; // last token pushed to the place of the one that was transferred tokenIndexInOwnerArray[swapToken] = index; // update the index of the token we moved delete listOfOwnerTokens[msg.sender][length - 1]; // remove the case we emptied listOfOwnerTokens[msg.sender].length--; // shorten the array's length // We set the new owner of the token tokenIdToOwner[_tokenId] = _to; // we add the token to the list of the new owner listOfOwnerTokens[_to].push(_tokenId); tokenIndexInOwnerArray[_tokenId] = listOfOwnerTokens[_to].length - 1; Transfer(msg.sender, _to, _tokenId); }
虽然代码比较长,但是是转账过程中必不可少的一步。
还必须注意的是,ERC721还支持approve()和transferFrom()函数,所以我们必须在transfer函数内部添加其他的限制指令,这样当一个token有新的owner时,之前授权地址的token不能被转出,代码如下:
function transfer(address _to, uint _tokenId) public (_tokenId) { // ... approvedAddressToTransferTokenId[_tokenId] = address(0); }
矿业
基于以上两个标准,可能会面临相同的需求,要么生成同质代币,要么生成非同质代币,一般都是用一个叫做Mint()的函数来完成。
实现上述功能的代码如下:
function mint(address _owner, uint256 _tokenId) public (_tokenId) { // We make sure that the token doesn't already exist require(tokenIdToOwner[_tokenId] == address(0)); // We assign the token to someone tokenIdToOwner[_tokenId] = _owner; listOfOwnerTokens[_owner].push(_tokenId); tokenIndexInOwnerArray[_tokenId] = listOfOwnerTokens[_owner].length - 1; // We update the total supply of managed tokens by this contract totalSupply = totalSupply + 1; // We emit an event Minted(_owner, _tokenId); }
使用任何数字生成新令牌。 根据不同的应用场景,合约内一般只授权部分地址对其进行挖矿(铸币)操作。
这里需要注意的是,mint()函数并没有出现在协议标准的定义中,而是我们添加了它,也就是说我们可以对标准进行扩展,添加其他必要的对代币的操作。 例如,可以添加使用以太币买卖代币的系统,或者可以删除不再需要代币的功能。
元数据
如前所述,非同质代币是价值的代表,在大量情况下,这个价值是需要描述的。 这可以通过以下字符串实现:
mapping(uint => string) internal referencedMetadata;
可见,智能合约与其说是权利证明,不如说是包含了某个对象。 例如,汽车不能存储在智能合约中,但可以存储车牌或其他法律文件。
目前广泛应用于虚拟资产的技术都是使用IPFS hash作为元数据,IPFS hash是存储在IPFS系统中的一个文件的地址。 简单地说,IPFS 是 HTTP 的种子版本。 当一个新文件被添加到 IPFS 时,它会出现在 IPFS 网络中的至少一个计算节点上。
当文件通过 IPFS 或 HTTP 对所有人可见时,“所有权令牌证明”就会在智能合约中注册。 这个操作不是程序,而是不可替代代币的新应用。 它被称为“加密收藏品”,现在正变得越来越热。
回到我们的代码上,目前关于ERC721的讨论不是很活跃,原来的建议帖已经很久没有更新了,所以有一个新的讨论方案在此基础上,叫做ERC841。 在 ERC841 中,“non-fungible token”被“契约”的标题所取代。
另一种方案ERC821也被提出,期望在ERC223和ERC777的基础上提供更好的方案设计。
ERC821和ERC841目标一致,但实现方式略有不同,但都需要完善。 如果您有建议,可以参与讨论。
ERC20 和 ERC721 的实现(不推荐用于生产)可以在 Github 上找到:devzl/ethereum-walkthrough-4
此外,值得花一点时间了解 OpenZepplin 框架。 他们有很棒的模块化智能合约,大部分都经过审计(当然,最好在决定使用哪个模块之前通读内容)