在本系列的第一篇文章中 ,我介绍了非真伪令牌(NFT)的概念以及对ERC721(草案)标准的需求 。 在本文中,我们将首先介绍ERC721标准接口,并分解一些要求。 关于ERC165标准还将有一个简短但重要的绕道。
接口和ERC165标准
ERC721标准指出:
“每个符合ERC-721的合同都必须实现ERC721和ERC165接口”
如果您从来不需要编写一个Solidity协议,并与其他开发人员编写的合同一起工作,您可能会想知道“什么是接口 ?”。 不管你是否想知道,“什么是ERC621接口?”。 所以让我们回答这两个问题。
接口
一个接口基本上是一个抽象契约,但是你唯一可以定义的是未实现的函数。这是一个用Solidity代码编写的概要,它确保其他开发人员编写的合同能够很好地协同工作,而不必知道彼此的代码库。
例如,如果一个接口将函数balanceOf
定义为
函数balanceOf(地址_owner)外部视图返回(uint256);
那么你知道任何实现该接口的 契约都会有一个称为balanceOf
的函数,它接受一个参数( 地址 )并返回一个值( uint256 )。 你也知道这个函数的可变性是view,这意味着该函数能够读取合同状态,但不能修改它,并且它具有外部修饰符,这对气体消耗有影响,以及该功能应该如何调用。 如果您的契约函数的修饰符或返回值与接口中定义的值不匹配,则会导致编译器给出TypeErrors并且无法编译。
因此,只需使用一个界面 ,就可以告诉其他开发人员有关您的合同所具有的一些功能以及如何使用它们。 简单!
但是这提出了一个问题,没有看看你的合同代码,其他开发者怎么知道你已经使用了给定的接口 ? 这个接口的全部重点是我们不必知道对方的代码库。答案是: ERC165标准 。
ERC165标准
“什么?!? 另一个ERC标准? 这个兔子洞有多深?“
别担心,这是我们将要讨论的唯一一个,而且非常简单 - 它只有一个功能! ERC165标准只是一种检查您的合同指纹是否与任何给定接口的指纹相匹配的方法。 我们来看看整个ERC165标准接口:
界面ERC165 { /// @notice查询合同是否实现了一个接口 /// @param interfaceID接口标识符,如 ///在ERC-165中指定 /// @dev接口标识在中指定 /// ERC-165。 该功能使用不到30,000个气体。 /// @如果合约实现了`interfaceID`,@return`true` ///和`interfaceID`不是0xffffffff,否则为`false` 函数supportsInterface(bytes4 interfaceID)外部视图返回(bool); }
因此,您的合同必须有一个函数supportsInterface
,它接受一个表示interfaceID
( bytes4 )的参数,如果该接口受支持,则返回true
( bool),其中interfaceID
在ERC165标准中定义为“异或在界面中的所有功能选择器“。
或者用简单的英语,你可以给这个功能任何接口的指纹( interfaceID
),并且它会告诉你它是否与你的任何契约的手指相匹配。
至于获取函数选择器,有两种简单的方法来完成它。 作为一个例子,我将使用前面的balanceOf
函数。 为了节省你滚动,它被定义为:
函数balanceOf(地址_owner)外部视图返回(uint256){ // ... };
我们最终的ERC721合同已经实现了这个功能,这就是我添加花括号的原因,但是当涉及到函数选择器时,它并不重要 - 你会明白为什么。 获取上述函数的函数选择器的两种方法是:
this.balanceOf.selector
或手动使用
bytes4(keccak256( “balanceOf(地址)”))
两者都会返回0x70a08231
,虽然第一个看起来比较干净,但我们偶尔需要第二个,如果接口使用重载函数。 您会注意到函数选择器不关心参数名称,修饰符,可变性,返回值或函数的内容。 只是函数名称和参数类型。 这就是为什么我说这个功能是否被实现并不重要。
但是我们需要interfaceID中的“接口中所有函数选择器的XOR” 。 让我们假设一个接口由三个函数组成: function1()
, function2()
和function3()
。
interfaceID
只是:
interfaceID = this.function1.selector ^ this.function2.selector ^ this.function3.selector;
很简单! 现在请记住,我们开始进行ERC165讨论的原因是因为我们的ERC721协议需要实现ERC165接口,所以让我们来编写我们的ERC165实现(ERC721协议将继承它)来包装它。
当谈到Solidity时,我们想要减少使用的气体。 做不必要的计算会使用户花费金钱,并浪费网络资源。 ERC165标准实际上要求supportsInterface
功能“ 使用少于30,000个气体”。 因此,每次有人调用supportsInterface
,不要重新计算interfaceID,而是让我们支持的接口ID保存在映射中 。
开始我们的合同 , CheckERC165
,像这样:
合同CheckERC165是ERC165 { 映射(bytes4 => bool)internal supportedInterfaces;
这样, supportsInterface
函数只需要从映射中返回一个值,下面是整个函数的实现:
函数supportsInterface(bytes4 interfaceID)外部视图返回(bool){ 返回supportedInterfaces [interfaceID]; }
大! 我们的合同现在实现了ERC165接口中的所有功能(只有一个)。 让我们将ERC165 interfaceID
添加到supportedInterfaces
并将此事物带到一个完整的圆圈。
最近发布了Solidity Version 0.4.22,为我们的constructor
函数提供了更简单的constructor
函数名称。 所以让我们来构造一个构造函数,并在其中添加ERC165接口的interfaceID。
构造函数()public { supportedInterfaces [this.supportsInterface.selector] = true; }
现在,如果有人使用ERC165标准接口 ( 0x01ffc9a7
)的interfaceID
调用supportsInterface
,它将返回true
。
这就是ERC165! 完整的合同可在我的GitHub上找到 。 我们稍后将使用ERC721实现,在处理一般接口时,它也是一个很方便的实现。
ERC721接口
现在我们已经了解了接口,让我们回到ERC721。 您会注意到ERC721标准实际上包含四个不同的接口。 一般ERC721合同的一个主要部分,一个用于可以接收ERC721令牌的合同,另外两个用于增加额外功能的可选扩展。 我们现在将忽略扩展,并快速查看主界面和接收器。
应付职能和可变性
立即对我而言突出的是ERC721接口中的四个功能具有应付修饰符。 即两个safeTransferFrom
函数, transferFrom
和approve
。 每次转移令牌或授予令牌控制权时,ERC721合同总是应付款并没有意义。
你可以想象这种情况可能是这种情况 - 如果你为NFT建立市场,那么毫无疑问你会处理付款 - 但是令牌所有者必须支付的情况只是为了将令牌的控制权交给某人否则很难想象。 事实上, 应付修饰词的原因并不是很性感,这一切都归结为可变性。
从ERC721标准的注意事项部分:
“易变性保证是弱到强:payable
,隐含不付款,view
和pure
。”
因此,通过将这些功能标记为可payable
,这仅仅是作者的一种说法,即对所需的可变性没有限制。 在这个特定的例子中, payable
只是一种明确表示没有限制的方式。 您的实施可能会更严格,但不会更弱。
有两种方法可以解决这个问题,你可以:
- 保持这些功能为可支付的,只需返回任何随事务发送的Ether(或保留它,如果这是您的设计),或者
- 删除应付修饰符,但您必须在ERC721 接口副本中执行相同的操作 ,以避免引发TypeError。
从前面记得,函数选择器不受修饰符的影响,所以这不会干扰interfaceID。标记为external
功能也是如此,您应该随时根据需要将其更改为public
。
ERC721TokenReceiver
在结束之前,我想快速介绍ERC721TokenReceiver
接口。 这不是我们令牌合同需要继承的东西。 顾名思义,它是可以接收ERC721令牌的合同接口。
由于我不会在本系列中介绍制作钱包合约,因此可以快速制作一些虚拟钱包,稍后我们将使用这些虚拟钱包进行测试。 一个有效的接收者和一个无效的接收者。
就我们的目的而言,唯一重要的信息是有效的ERC721TokenReceiver
将实现该功能
函数onERC721Received(地址_from,uint256 _tokenId,字节数据)外部返回(bytes4);
并返回
bytes4(keccak256(“onERC721Received(address,uint256,bytes)”))
一个无效的将不执行该函数,或者从字面上返回任何其他内容。 所以让我们定义我们的两个接收器如下:
合同ValidReceiver是ERC721TokenReceiver { 函数onERC721Received(地址_from,uint256 _tokenId,字节数据)外部返回(bytes4){ 返回bytes4(keccak256(" onERC721Received(address,uint256,bytes) ")); }
bytes4(keccak256(" onERC721Received(address,uint256,bytes) ")); }
}
和
合同InvalidReceiver是ERC721TokenReceiver { 函数onERC721Received(地址_from,uint256 _tokenId,字节数据)外部返回(bytes4){ 返回bytes4(keccak256(" some invalid return data ")); }
bytes4(keccak256(" some invalid return data ")); }
}
我们不会在很久之后才会使用这些测试,所以请保存它们并放在口袋里。 我只是想在我们对接口进行概述的同时对其进行介绍。
包起来
所以毕竟,你应该熟悉接口,并且有一个ERC165实现,你可以使用它来指示你的合约实现了哪些接口。 我们还介绍了ERC721标准中的可变性保证,并且很快写了一些虚拟钱包合约以供日后测试。
在下一篇文章中,我们将使用到目前为止所介绍的所有内容,并开始编写实际的ERC721令牌合同。
https://medium.com/coinmonks/jumping-into-solidity-the-erc721-standard-part-2-383438734de5