【CITA 智能合约开发系列】智能合约场景

智能合约流行场景: Token 和 ERC 标准

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

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

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

ERC 20 标准

ERC20(https://theethereum.wiki/w/index.php/ERC20_Token_Standard)是为同质(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);}

标准不提供功能的实现,这是因为大家可以用自己喜欢的方式写出任何代码,如果不需要提供某些功能只需要按照标准(https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md)返回 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 代币标准的核心内容。

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

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

ERC 721 标准

ERC721目前看,ERC721ERC721跟 ERC20 及其近亲系列有本质上的不同。ERC721 中,代币都是唯一的。ERC721 提出来后的众多使用案例中,CryptoKittiesCryptoKitties,这款使用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() 函数并没有出现在协议标准定义中,而是我们添加上去的,也就是说我们可以对标准进行扩充,添加其它对代币的必要操作。例如,可以添加用以太币来买卖代币的系统,或者删除不再需要代币的功能。

系列文章:

  1. 智能合约定义
    智能合约的历史和定义, 介绍在cita上使用智能合约
  2. 智能合约开发
    智能合约的基础语法介绍,cita ide编辑器介绍,如何cita开发Dapp
  3. 智能合约安全性
    智能合约一些高级技巧
  4. 智能合约场景(本文)
    两种智能合约代币ERC20,ERC721的标准接口讲解
1赞