NFT:4 种最常见的 NFT 合约设计缺陷

原文标题:4 Common NFT Contract Design Anti-Patterns

原文作者:@kralizec

原文来源:hackernoon.com

编译:ChinaDeFi

我们已经见到了NFT热潮,而且这个热潮很有可能会一直延续下去。Etherscan有一个使用起来非常方便的搜索实用程序,它有验证和反编译功能,可以让我们查看许多ERC721的代码来进行比较。除了许多精心设计的合约,我们其实也可以看到许多合约会反复犯同样的错误。在这篇文章中,我将给出个人认为的4个最常见的NFT“设计缺陷”,这是我在Etherscan上查看NFT合约时经常注意到的。

本文主要考虑的是与EVM兼容的区块链,但其中的许多观点在其他网络上也适用或具有一些类比或等效性。

#1:在合约中包含价格/销售信息和逻辑

这是非常常见的,但同时,它标志着合约很业余。这样做有一些合理的和可理解的动机。首先,在许多网络上部署和管理合约已经变得非常昂贵,为了节省这些成本,人们已经算是煞费苦心。而且,为了简单起见,有人可能会想,为什么不把铸造和销售的逻辑放在合约本身呢?

但这真的不是一个好主意。合约本身应该是一个逻辑网络的不可变中心,不应该直接处理金钱。包括销售、销售时间、白名单等,它们直接在与ERC721实现相同的合约代码中。销售逻辑和核心逻辑是紧密耦合的。

合约

节省 gas 成本可能是将所有逻辑塞进一份合约的最佳和最容易理解的理由,但我认为,核心合约逻辑应该是唯一固定的东西,并且在大多数情况下需要以一种非常标准的方式实现标准。我们的铸造策略、定价都应该被分离开来。这使得我们的合约会以一种不损害用户信任的方式来变得灵活。附注:在ERC721合约本身中限制供应(即maxSupply)是有意义的,只要它可以由具有管理员角色的人进行修改。

合约

合约

#2:不实现基于角色的安全性

代币合约需要某种访问控制,因为有些函数(如铸造或对供应参数做任何事情)应该只对被允许的地址可用。最简单的方法是使用Ownable模型(通常使用OpenZeppelin的Ownable合约)。但是使用基于角色的访问控制是有必要的。使用Ownable(或类似的东西)背后的动机可能是简单(和节省gas成本),表面上看这很好。与Ownable模型相比,基于角色的安全性(如OpenZeppelin的IAccessControl)的复杂系数要更高(且昂贵)。如果gas成本仍然是一个问题,我们可以删除基于角色的安全代码,只保留我们需要的内容。但是使用基于角色的安全性的更重要的原因是,它使我们能够将功能(如前面提到的点、销售和定价信息)与ERC721合约本身分离开来。它允许我们通过为其分配“铸造者”角色来指定一个单独的合约作为铸造者,而不允许它拥有完整的管理员权限。而管理员仍然拥有更高级别的权限(例如删除和添加权限)。当铸造者(例如)不再满足我们的需求时,我们只需撤销它的铸造权,并将铸造权分配给一个实现新的铸造策略的新合约就可以;它是模块化的,方便的,安全的。基于项目特定的用例,铸造之外的其他活动也可以以相同的方式进行处理。

合约

合约

#3:没有正确实现ERC-165

许多代币(或一般的合约)要么没有实现ERC-165,要么没有优化地实现它。ERC-165是关于互操作性的。它使我们的合约与未来相兼容,交易所可能会调用它来了解(例如)我们的NFT的版税结构。我经常看到这一点根本没有被执行,或者执行得不是很理想。

以下是正确实现它的法则:

  • 任何实现ERC-165的父类都应该覆盖列表。然后,当我们调用 super.supportsInterface 时,它们将被自动调用。
  • 任何其他未在父类中表示的已实现接口,都可以使用 or 子句添加,如下所示:

|| type(ISomeInterface).interfaceId == _interfaceId

例子:

function supportsInterface(bytes4 _interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool){ return super.supportsInterface(_interfaceId) || _interfaceId == type(IERC2981).interfaceId; }

如果我们的代码没有实现ERC-165的父类,那么应该只表示第二种类型,例如:

function supportsInterface(bytes4 _interfaceId) public view override returns (bool){ return _interfaceId == type(IERC721).interfaceId || _interfaceId == type(IERC2981).interfaceId || _interfaceId == type(IAccessControl).interfaceId; }

如果我们的代码除了由ERC-165的父类实现处理的接口之外没有实现其他接口,那么就不需要第二种类型。如:

function supportsInterface(bytes4 _interfaceId) public view override(ERC721, ERC721Enumerable) //just make sure this list is complete returns (bool){ return super.supportsInterface(_interfaceId); }

正确实现ERC-165是可选的,但也是重要的。我们希望自己的代币与尽可能多的其他系统(如交易所)兼容,包括未来尚未实现的系统。随着时间的推移和空间的成熟,ERC-165标准可能会变得更加常用和重要。

#4:部署前不进行彻底测试

我们的ERC721代币可能是非常标准的,并且可能使用所有第三方父类和库,很少进行自定义,而且我们都认为第三方代码经过了良好的测试。但我们仍然需要彻底测试自己的代码,因为在将其部署到主网之前,我们只有一次机会将其正确地编写出来。

当然,首先是单元测试。我们使用什么测试框架并不重要;我用hardhat来测试以太和mocha。即使我们可能正在测试的代码(例如OpenZeppelin)已经是众所周知的测试良好的代码,但,(a)我们的自定义代码可能已经破坏了某些情况,所以它们应该重新被测试,(b) OpenZeppelin以前就有bug,将来可能还会有。为了节省我们的时间,我们可以为所有ERC721代币、所有ERC20代币、所有ERC1155代币等准备一套标准测试套件,并使他们可以在项目之间重复使用。我们也可以为每个项目添加案例,以覆盖对标准的任何自定义。单元测试应该涵盖访问控制、基本功能(如生成和传输)、可暂停性(如果你的合约是可暂停的)、ERC165标准的实现等等。我们可以使用solidity-coverage(一个nodejs包)来测试覆盖率。

最后,自动化工具可以在测试中为我们提供大量的帮助。Slither、Manticore和Mythril是行业标准,通常由Consensys和Certik等主要安全审计公司使用。Solidity -coverage (一个 nodejs 包)将告诉我们单元测试提供的估计覆盖率百分比。Solgraph是一个工具,可以帮助我们看到合约代码中的关系和联系;在测试计划中有用。

> pip3 install slither-analyzer> pip3 install mythril > npm install solidity-coverage

总结

  • 彻底、深入的单元测试得到尽可能多的成功用例、异常用例和边缘用例。
  • 人工测试和检查。
  • 使用自动化工具,如slither、manticore、mythril、echidna、solidity-coverage。

责任编辑:Felix

转载声明:本文 由CoinON抓取收录,观点仅代表作者本人,不代表CoinON资讯立场,CoinON不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。若以此作为投资依据,请自行承担全部责任。

声明:图文来源于网络,如有侵权请联系删除

风险提示:投资有风险,入市需谨慎。本资讯不作为投资理财建议。

(0)
打赏 微信扫一扫 微信扫一扫
上一篇 2022年8月29日 下午7:22
下一篇 2022年8月29日 下午7:24

相关推荐

NFT:4 种最常见的 NFT 合约设计缺陷

星期一 2022-08-29 19:22:49

我们已经见到了NFT热潮,而且这个热潮很有可能会一直延续下去。Etherscan有一个使用起来非常方便的搜索实用程序,它有验证和反编译功能,可以让我们查看许多ERC721的代码来进行比较。除了许多精心设计的合约,我们其实也可以看到许多合约会反复犯同样的错误。在这篇文章中,我将给出个人认为的4个最常见的NFT“设计缺陷”,这是我在Etherscan上查看NFT合约时经常注意到的。

本文主要考虑的是与EVM兼容的区块链,但其中的许多观点在其他网络上也适用或具有一些类比或等效性。

#1:在合约中包含价格/销售信息和逻辑

这是非常常见的,但同时,它标志着合约很业余。这样做有一些合理的和可理解的动机。首先,在许多网络上部署和管理合约已经变得非常昂贵,为了节省这些成本,人们已经算是煞费苦心。而且,为了简单起见,有人可能会想,为什么不把铸造和销售的逻辑放在合约本身呢?

但这真的不是一个好主意。合约本身应该是一个逻辑网络的不可变中心,不应该直接处理金钱。包括销售、销售时间、白名单等,它们直接在与ERC721实现相同的合约代码中。销售逻辑和核心逻辑是紧密耦合的。

合约

节省 gas 成本可能是将所有逻辑塞进一份合约的最佳和最容易理解的理由,但我认为,核心合约逻辑应该是唯一固定的东西,并且在大多数情况下需要以一种非常标准的方式实现标准。我们的铸造策略、定价都应该被分离开来。这使得我们的合约会以一种不损害用户信任的方式来变得灵活。附注:在ERC721合约本身中限制供应(即maxSupply)是有意义的,只要它可以由具有管理员角色的人进行修改。

合约

合约

#2:不实现基于角色的安全性

代币合约需要某种访问控制,因为有些函数(如铸造或对供应参数做任何事情)应该只对被允许的地址可用。最简单的方法是使用Ownable模型(通常使用OpenZeppelin的Ownable合约)。但是使用基于角色的访问控制是有必要的。使用Ownable(或类似的东西)背后的动机可能是简单(和节省gas成本),表面上看这很好。与Ownable模型相比,基于角色的安全性(如OpenZeppelin的IAccessControl)的复杂系数要更高(且昂贵)。如果gas成本仍然是一个问题,我们可以删除基于角色的安全代码,只保留我们需要的内容。但是使用基于角色的安全性的更重要的原因是,它使我们能够将功能(如前面提到的点、销售和定价信息)与ERC721合约本身分离开来。它允许我们通过为其分配“铸造者”角色来指定一个单独的合约作为铸造者,而不允许它拥有完整的管理员权限。而管理员仍然拥有更高级别的权限(例如删除和添加权限)。当铸造者(例如)不再满足我们的需求时,我们只需撤销它的铸造权,并将铸造权分配给一个实现新的铸造策略的新合约就可以;它是模块化的,方便的,安全的。基于项目特定的用例,铸造之外的其他活动也可以以相同的方式进行处理。

合约

合约

#3:没有正确实现ERC-165

许多代币(或一般的合约)要么没有实现ERC-165,要么没有优化地实现它。ERC-165是关于互操作性的。它使我们的合约与未来相兼容,交易所可能会调用它来了解(例如)我们的NFT的版税结构。我经常看到这一点根本没有被执行,或者执行得不是很理想。

以下是正确实现它的法则:

  • 任何实现ERC-165的父类都应该覆盖列表。然后,当我们调用 super.supportsInterface 时,它们将被自动调用。
  • 任何其他未在父类中表示的已实现接口,都可以使用 or 子句添加,如下所示:

|| type(ISomeInterface).interfaceId == _interfaceId

例子:

function supportsInterface(bytes4 _interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool){ return super.supportsInterface(_interfaceId) || _interfaceId == type(IERC2981).interfaceId; }

如果我们的代码没有实现ERC-165的父类,那么应该只表示第二种类型,例如:

function supportsInterface(bytes4 _interfaceId) public view override returns (bool){ return _interfaceId == type(IERC721).interfaceId || _interfaceId == type(IERC2981).interfaceId || _interfaceId == type(IAccessControl).interfaceId; }

如果我们的代码除了由ERC-165的父类实现处理的接口之外没有实现其他接口,那么就不需要第二种类型。如:

function supportsInterface(bytes4 _interfaceId) public view override(ERC721, ERC721Enumerable) //just make sure this list is complete returns (bool){ return super.supportsInterface(_interfaceId); }

正确实现ERC-165是可选的,但也是重要的。我们希望自己的代币与尽可能多的其他系统(如交易所)兼容,包括未来尚未实现的系统。随着时间的推移和空间的成熟,ERC-165标准可能会变得更加常用和重要。

#4:部署前不进行彻底测试

我们的ERC721代币可能是非常标准的,并且可能使用所有第三方父类和库,很少进行自定义,而且我们都认为第三方代码经过了良好的测试。但我们仍然需要彻底测试自己的代码,因为在将其部署到主网之前,我们只有一次机会将其正确地编写出来。

当然,首先是单元测试。我们使用什么测试框架并不重要;我用hardhat来测试以太和mocha。即使我们可能正在测试的代码(例如OpenZeppelin)已经是众所周知的测试良好的代码,但,(a)我们的自定义代码可能已经破坏了某些情况,所以它们应该重新被测试,(b) OpenZeppelin以前就有bug,将来可能还会有。为了节省我们的时间,我们可以为所有ERC721代币、所有ERC20代币、所有ERC1155代币等准备一套标准测试套件,并使他们可以在项目之间重复使用。我们也可以为每个项目添加案例,以覆盖对标准的任何自定义。单元测试应该涵盖访问控制、基本功能(如生成和传输)、可暂停性(如果你的合约是可暂停的)、ERC165标准的实现等等。我们可以使用solidity-coverage(一个nodejs包)来测试覆盖率。

最后,自动化工具可以在测试中为我们提供大量的帮助。Slither、Manticore和Mythril是行业标准,通常由Consensys和Certik等主要安全审计公司使用。Solidity -coverage (一个 nodejs 包)将告诉我们单元测试提供的估计覆盖率百分比。Solgraph是一个工具,可以帮助我们看到合约代码中的关系和联系;在测试计划中有用。

> pip3 install slither-analyzer> pip3 install mythril > npm install solidity-coverage

总结

  • 彻底、深入的单元测试得到尽可能多的成功用例、异常用例和边缘用例。
  • 人工测试和检查。
  • 使用自动化工具,如slither、manticore、mythril、echidna、solidity-coverage。

责任编辑:Felix