到目前为止,在本系列中,我介绍了非易失性和ERC721的基础知识 ,然后介绍了标准接口及其中的一些要求 。 在本文中,我们将对我们的ERC721合同做出一些设计决定,并开始编写它。
设计选择,可扩展性和安全性
正如本系列第1部分所讨论的, ERC721标准适用于管理以太坊区块链上不可替代资产的合同。 ERC721代币所代表的资产将影响您的合同工作方式的一些设计选择,最显着的是如何创建新代币。
例如,在Cryptokitties游戏中,玩家可以“培育”他们的Kitty,从而创造出新的Kitty(代币)。 然而,如果您的ERC721令牌代表更有形的东西,比如演唱会门票,您可能不希望令牌持有者能够创建更多的令牌。 在某些情况下,你甚至可能希望代币持有者能够“烧”他们的代币,有效地摧毁他们。
ERC721标准对于如何创建或刻录令牌或谁可以创建它们没有限制,因此您可以根据自己的需要自由做出这些决定。
设计选择
在我的合同中,为了可扩展性和简单性,令牌将以两种方式创建:
- 在令牌创建期间将定义初始的令牌供应,所有这些供应最初都属于合同创建者。
- 一个只能由合约创建者调用的函数,在调用时会发出更多的令牌。 这些新的令牌最初也属于合同创建者。
你会看到它如何适应不久的可扩展性。 至于刻录,将会有一个函数烧毁一个只能由令牌所有者(或授权的操作员)调用的给定令牌。
可扩展性
由于每个令牌都有一个所有者,因此我将有一个跟踪令牌所有权的映射,定义为:
映射 ( uint256 => 地址 ) 内部所有者;
该标准说令牌不能属于零地址( 0x0
),所以在所有者映射中 ,我们可以让0x0
回退到合同创建者。 当我们编写ownerOf
函数时,我会详细ownerOf
它,但这意味着如果我们发行10个标记, ownerOf
每个标记的所有权明确地设置为合同创建者。
为了简单起见,因为属于0x0
令牌会回落到合同创建者身上,我不会让令牌合同被转移给其他所有者。
安全
在我们开始之前,我们需要覆盖的最后一件事非常重要 - 防止溢出。 如果你不知道溢出,整数类型有它可以容纳的最大值和最小值。 在uint256的情况下,它可以容纳的最小值是0,它可容纳的最大值是115792089237316195423570985008687907853269984665640564039457584007913129639935。
如果你有一个uint256的值为0,你从它减去1,它的价值将是那个大疯狂的数字。 如果你加1,它将再次为0。 基本上uint不能算更高的数字,所以它只是从底部开始。 将两个数字相乘的结果大于变量可容纳的最大值时,您会得到相似的结果。
如果你不小心,恶意(或愚蠢)的人可以利用这种行为来对你的代码做各种令人讨厌的事情。 就在上周,由于与溢出相关的令牌合约存在漏洞,一些ERC20令牌在许多交易所中被从交易中移除。 有人聪明地找到了一种方法来发送他自己实际上没有的疯狂数量的令牌,然后迅速将它们出售。 对于受影响的人来说,这是绝对的混乱。
但是不要绝望, OpenZeppelin的那些人已经写了一个库来帮助防止这种攻击。 SafeMath.sol用于仔细检查在重要计算过程中没有发生溢出。
通过声明using SafeMath for uint256;
在合同的顶部,这意味着以下计算:
c = a + b
存在溢出风险,变成:
c = a.add(b);
如果它溢出会抛出。 这比一般的增加成本要高一些,所以我们应该在SafeMath操作存在溢出的可能性(来自不可预知的用户输入 - 包括合同所有者/创建者!)时保存操作。
注意:我在本合同中使用SafeMath的唯一时间是发布额外令牌。 所以如果你的合同设计不同,你可能不需要使用这个库; 然而,这是一个非常有用的项目,在你的工具带上。
合同
我们终于准备好开始编写我们的合同,所以让我们直接去做。 我们将使用Solidity 0.4.22,并列出依赖关系。
杂注扎实度0.4.22;
导入 “./CheckERC165.sol”; 导入 “./standard/ERC721.sol”; 导入 “./standard/ERC721TokenReceiver.sol”; 导入 “./libraries/SafeMath.sol”;
关于ERC721.sol(ERC721标准接口)的快速注释,我对某些函数的可变性和可见性做了一些细微的修改。 但正如前一篇文章中提到的那样,这是标准所允许的。 我将在他们出现时讨论这些更改,并且所有文件都可以在我的GitHub上找到 。
接下来我们宣布我们的合同 ,记住要扩展ERC721 接口和我们的ERC165实现,并且包括适用于uint256的SafeMath 。
合同 TokenERC721 是 ERC721,CheckERC165 { 使用 SafeMath for uint256 ;
变量
在契约顶部声明合同变量总是一个很好的做法,因为它使得阅读起来更容易。 现在我会对他们全部进行闪光,并对每种情况做一个简短的解释,但是当我们真正使用它们时,我会进一步细化。 注意:我使用内部可见性而不是私有的 ,以便扩展我们的令牌合约(如“元数据”和“可枚举”变体)的合约可以访问它们。
//合同创建者的地址 解决内部创建者;
//最高有效的tokenId,用于检查tokenId是否有效 uint256内部 maxId;
//存储每个地址的余额的映射 映射 ( 地址 => uint256 ) 内部余额;
// 刻录令牌的映射,用于检查tokenId是否有效 //如果您的令牌无法被烧毁,则不需要 映射 ( uint256 => bool ) 内部烧毁;
//令牌所有者的映射 映射 ( uint256 => 地址 ) 内部所有者;
//每个令牌的“批准”地址映射 映射 ( uint256 => 地址 ) 内部补贴;
//用于管理“运营商”的嵌套映射 映射 ( 地址 => 映射 ( 地址 => 布尔 )) 内部授权;
构造函数
接下来我们将定义构造函数。 根据你的令牌需要做什么,你的构造函数很可能与我的不同。 但是,无论如何你仍然应该包括ERC165部分。 我会在之后讨论一些怪癖。
构造函数( uint _initialSupply) public CheckERC165(){ //存储创建者的地址 creator = msg.sender; //所有初始令牌都属于创作者,因此请设置天平 余额[msg.sender] = _initialSupply;
//将maxId设置为令牌的数量 maxId = _initialSupply;
//我们必须为我们创建的每个标记发出一个事件 for ( uint i = 1; i <= maxId; i ++){ 发送Transfer(0x0,创建者,i); }
你们中的一些人可能已经看到了maxId = _initialSupply
并立即想到“这是一个错字吗? 或者tokenIds从1开始而不是0? 什么样的反社会人士从1开始索引?!?“
不,这不是一个错字。 我选择在1开始tokenIds的原因是,当您开始向合约添加额外层(超出ERC721标准的范围)时,0非常方便。 由于uint的默认值为0,并且对变量使用删除操作会给出气体退款,因此它可以是对令牌的引用无效的简写方式。 实际上,tokenId 不是索引。 标准只要求每个令牌都有自己独特的uint ,永远不会改变,它并不太在乎你如何确定它们。 引用标准:
“虽然一些ERC-721智能合约可能会发现从ID 0开始并为每个新的NFT简单地增加一个便利,但呼叫者不应假定ID号码对他们有任何特定模式,并且必须将该ID作为”黑色框'。”
您可能还注意到,对于interfaceId
,我使用了模式bytes4 (keccak256(“...
用于两个函数签名),这是因为ERC721 Standard 接口重载safeTransferFrom
,所以只调用this.safeTransfeFrom.selector
将导致TypeError 。
有效令牌
所有_tokenId
作为参数的函数都要求我们检查这个tokenId是否对应于一个有效的标记。 所以让我们编写一个可重用的内部函数来检查。
函数 isValidToken( uint256 _tokenId) 内部视图返回 ( bool ){ return _tokenId!= 0 && _tokenId <= maxId &&!burned [_tokenId]; }
如果给定的_tokenId
为0,大于maxId
或对应于烧毁的标记,这将返回false
。 如果您的令牌不允许刻录,则可以删除&& !burned[_tokenId]
部分。
平衡和所有者
接下来介绍两个基本的getter函数balanceOf
和ownerOf
。 他们很简单, 一个返回给定地址的余额,另一个返回给定令牌的所有者。
balanceOf
很简单,它只是从我们的余额映射中读取一个值:
函数 balanceOf( 地址 _owner) 外部视图返回 ( uint256 ){ 回报余额[_owner]; }
ownerOf
有点复杂:
函数 ownerOf( uint256 _tokenId) 公共视图返回 ( 地址 ){ 要求(isValidToken(_tokenId)); 如果 (所有者[_tokenId]!= 0x0){ 返回所有者[_tokenId]; } else { 回报创造者; } }
首先我们使用我们之前的isValidToken
函数来检查它是否是有效的标记。因为我使用0x0
的所有者将令牌返回给合同创建者,所以我还必须为此添加支票。 但取决于您的合同设计,您可能可以省略此检查并仅返回owners[_tokenId]
。
还要注意,我在这里和界面中都改变了可见性,从外部到公共 。 这是因为我们的ownerOf
函数最终会在我们的合同中被相当频繁地调用,而且如果它不是外部函数,则会更便宜。
问题和烧伤
我今天要添加的最后一件事就是我的实现的问题和刻录功能。 除非您正在复制我的实现(欢迎您这样做),否则这些内容不太可能有用。 然而,为了让我可以把下一篇文章的全部内容用于审批,运营商和转账,我现在将介绍他们:
函数 issueTokens(uint256_extraTokens) public { //确保只有合约创建者可以调用它 require(msg.sender == creator); 余额[msg.sender] =余额[msg.sender]。 添加 (_extraTokens); maxId = maxId。 添加 (_extraTokens); //我们必须为每个创建的标记发出一个事件 for ( uint i = maxId - _extraTokens + 1; i <= maxId; i ++){ 发送Transfer(0x0,创建者,i); } }
请注意,在这两种情况下,我都使用了SafeMath的add函数。 否则,一个足够大的_extraTokens值可能会导致各种破坏。
函数 burnToken( uint256 _tokenId) external { 地址所有者= ownerOf(_tokenId); 要求(所有者== msg.sender || 津贴[_tokenId] == msg.sender || 授权[所有者] [msg.sender] ); 烧毁[_tokenId] = true ; 结余[所有者] - ;
发射传输(所有者,0x0,_tokenId); }
不要过于担心allowance
或authorised
,我会在下一篇文章中详细解释它们。 我们也会在那里看到这个确切的模式,所以我可以用更多的上下文来解释它。
包起来
今天我们介绍了一些设计选择,以及了解溢出的危险以及如何防止溢出。 如果你一直在和我一起写自己的合同,现在应该开始形成。 我们已经基本建立了框架,接下来要做的就是添加移动部分。
在下一篇文章中,我们将通过添加批准,操作员和传输函数的不同变体来完成我们的合同。
https://medium.com/coinmonks/jumping-into-solidity-the-erc721-standard-part-3-5f38e012248b