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代码库中找到一些好的实现方式。另外,在以太坊上也有一个简易教程。