教程 | 以太坊开发演练,Part-4:代币与 ERC

hongji   |     |   2399 次阅读


以太坊开发演练系列:

教程 | 以太坊开发演练,Part-1:智能合约
教程 | 以太坊开发演练,Part-2:Truffle,Ganache,Geth 和 Mist
教程 | 以太坊开发演练, Part-3:安全性、限制性以及一些顾虑


从一个开发者角度来看,以太坊代币仅仅就是智能合约。如果拿饮料来对比,就如同任何人都可以用自己的方式调配出适合自己口味的咖啡。

大家也可能听说过 ERC20,ERC721,或者其它标准,这些都是开发者社区共同遵守的基础功能,在此基础上,大家都可以使用自己开发功能,以自己的方式创建脚本来管理虚拟货币。

加勒比海盗中有一句台词很好地再现了这个情况:

-代码更像是“指南”,而非实际规则-

长远看,遵循标准有很多不应忽视的益处。首先,如果遵照某个标准生成代币,那么每个人都会知道该代币的基础功能,并知道如何与之交互,因此就会有更多信任。去中心化程序(DApps)比如 Mist,可以直接辨别出其代币特征,并通过特定的 UI 来与其打交道。另外,一种代币智能合约的标准实现已经被社区开发出来,它采用类似 OpenZeppelin 的架构。这种实现已经被很多大神验证过,可以用来作为代币开发的起点。

本文中会从头开始提供一个不完整的,但是遵循 ERC20 标准的,基础版的代币实现,然后将它转换成遵循 ERC721 标准的实现。这样就能让读者看出两个标准之间的不同。

写本文的出发点是希望大家了解代币是如何工作的,其过程并不是一个黑箱;另外,对于 ERC20 这个标准,尽管它至少已经被广泛接受两年以上,如果只是从标准框架简单地生成自己的代币,也还会存在某些不易发现的故障点。

生成自己代币

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 的值就可以了。

注意:本文并不很强调代码,大家只需了解内部机理,全部代码将会在文末附上链接。

实现

首先,需要给代币起一个名字,因此会采用一个公有变量(Public Variable)’:

string public name = "Our Tutorial Coin";

然后给代币起一个代号:

string public symbol = "OTC";

当然还要有具体小数位数:

uint8 public decimals = 2;

因为 Solidity 并不完全支持浮点数,因此必须把所有数表示成整数。例如,对于一个数字 “123456”,如果使用 2 位小数,则代表 “1234.56”;如果采用4位小数,则代表 “12.3456”。0 位小数代表代币不可分。而以太坊的加密币以太币则使用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() 函数花费源地址里的代币。allowance() 只是一个从其它地址获得可用额度的函数。

这些函数都有安全隐患,因为当源地址向被授权地址授信可以花费 X 个代币后,有突发原因需要将授信改成 Y 个代币(提高或降低额度),被授权地址很有可能在重新授信执行之前将 X 个代币转移走;当 Y 个代币的新授信完成后,又可以转移Y个代币。在之前的本系列文章中我提到过,当某个交易正处在挖矿过程中时状态不能确定,矿工可以在交易挖矿过程中控制执行时间。

鉴于 ERC20 还存在其他一些问题,更安全容错的 transferFrom() 实现和其它方案被发布出来(如之前所说,该标准只是一些功能原型和行为定义,具体细节则靠开发者自己实现),并正在讨论中,其中就包括 ERC223ERC777.

ERC223 方案的动机是避免将代币发送到错误地址或者不支持这种代币的合约上,成千上万的金钱因为上述原因丢失,这一需求作为以太坊后续开发功能的第 223 条记录在案。ERC777 标准在支持其它功能的同时,对接收地址进行“即将收到代币”的提醒功能,ERC777 方案看起来很有可能替代 ERC20.

ERC721

目前看,ERC721 跟 ERC20 及其近亲系列有本质上的不同。

ERC721 中,代币都是唯一的。ERC721 是几个月前提出来的方案,CryptoKitties,这款使用ERC721标准实现的收集虚拟猫游戏使得它备受瞩目。以太猫游戏实际就是智能合约中的非同质代币 (non-fungible token),并在游戏中用猫的形象来表现出来。

如果想将一个 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 函数内部加上其它限制指令,这样一来,当某个代币有了新的拥有者,之前的被授权地址就无法其代币进行转移操作,代码如下:

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)操作。

这里需要注意,mint() 函数并没有出现在协议标准定义中,而是我们添加上去的,也就是说我们可以对标准进行扩充,添加其它对代币的必要操作。例如,可以添加用以太币来买卖代币的系统,或者删除不再需要代币的功能。

元数据

如前所述,非同质代币是价值的代表,大量情况下,需要描述这种价值。可以用如下字符串实现:

mapping(uint => string) internal referencedMetadata;

由此可见,智能合约与其说内含某种对象不如说是一种权益的证明。例如,不能将一辆车存放在智能合约中,但是可以存放车牌或者其它法律票证。

目前虚拟资产广泛使用的技术都用 IPFS 哈希作为元数据,IPFS 哈希是存放在 IPFS 系统中文件的地址。简单说,IPFS 是一个 HTTP 的 torrent 版本。当一个新文件添加到 IPFS 中,就会在 IPFS 网络中的至少一个计算节点上表现出来。

当文件通过 IPFS 或者 HTTP 对每个人都可见时,“代币所有权证明”就在智能合约中注册。这个操作不是程序,而应该是不可替代代币的一种新应用。它被称为“Crypto-collectibles”,现在变得很热门。

回到我们的代码,ERC721 的讨论目前不太活跃了,原始建议贴很久都没有更新过,因此基于此又有新的讨论方案,被称为 ERC841。在 ERC841 中“不可替代代币(non-fungible token)”被“契据(deed)”的称呼替代。

另外一个方案,ERC821,也被提出来,期望基于 ERC223 和 ERC777 提供更好的方案设计。

ERC821 和 ERC841 有同样的目标,但是实现方法上有些许不同,但都有待改进,如果大家有建议,可以参与讨论。

可以在 Github 上找到 ERC20 和 ERC721 的实现(不建议用于生产),链接为:devzl/ethereum-walkthrough-4

另外,花点儿时间了解 OpenZepplin 框架也是值得的。他们有非常棒的基本上通过了审计的模块化智能合约(当然,在你决定使用哪个模块之前最好通读其内容)

以上就是第四部分的内容,下一篇中我们将介绍如何创建 DApp

如果喜欢本文,可以通过如下方式联系我:@dev_zl

彩蛋

Initial coin offerings 有点儿偏离以太坊项目开发的议题,但是本质上,它就是一种众筹。

如果一个初创公司需要资金,就可以创建自己的代币,过一段时间卖一部分,被称做 crowdsale 或者 Initial coin offering。

在智能合约和区块链技术出现之前,初创公司会使用众筹网站集资,但是这种网站会抽走很大一部分服务费。有了 Initial coin offering 之后,没有了中间商,筹集的钱都归初创公司自己用了。

目前,集资项目更多的是骗局,从投资者角度看,应该看好自己的钱袋。从开发者角度看,crowdsale 就是一种智能合约,它卖的是未来兑换以太币的代币。没有一个标准方式,但是可以从OpenZepplin代码库中找到一些好的实现方式。另外,在以太坊上也有一个简易教程


原文链接: https://hackernoon.com/ethereum-development-walkthrough-part-4-tokens-and-ercs-68645cf2f73e
作者: dev_zl
翻译&校对: 小野于林 & Elisa

本文由作者授权 EthFans 翻译及再出版。


你可能还会喜欢:

科普 | 理解ERC-20 token合约
干货 | 代币支付的以太坊智能服务
白皮书 | 以太猫白皮书

 
0 人喜欢