本文主要考虑的是与EVM兼容的区块链,但其中的许多观点在其他网络上也适用或具有一些类比或等效性。
原文标题:4 Common NFT Contract Design Anti-Patterns
原文作者:@kralizec
原文来源:hackernoon.com
编译:ChinaDeFi
我们已经见到了NFT热潮,而且这个热潮很有可能会一直延续下去。Etherscan有一个使用起来非常方便的搜索实用程序,它有验证和反编译功能,可以让我们查看许多ERC721的代码来进行比较。除了许多精心设计的合约,我们其实也可以看到许多合约会反复犯同样的错误。在这篇文章中,我将给出个人认为的4个最常见的NFT“设计缺陷”,这是我在Etherscan上查看NFT合约时经常注意到的。
本文主要考虑的是与EVM兼容的区块链,但其中的许多观点在其他网络上也适用或具有一些类比或等效性。
这是非常常见的,但同时,它标志着合约很业余。这样做有一些合理的和可理解的动机。首先,在许多网络上部署和管理合约已经变得非常昂贵,为了节省这些成本,人们已经算是煞费苦心。而且,为了简单起见,有人可能会想,为什么不把铸造和销售的逻辑放在合约本身呢?
但这真的不是一个好主意。合约本身应该是一个逻辑网络的不可变中心,不应该直接处理金钱。包括销售、销售时间、白名单等,它们直接在与ERC721实现相同的合约代码中。销售逻辑和核心逻辑是紧密耦合的。
节省 gas 成本可能是将所有逻辑塞进一份合约的最佳和最容易理解的理由,但我认为,核心合约逻辑应该是唯一固定的东西,并且在大多数情况下需要以一种非常标准的方式实现标准。我们的铸造策略、定价都应该被分离开来。这使得我们的合约会以一种不损害用户信任的方式来变得灵活。附注:在ERC721合约本身中限制供应(即maxSupply)是有意义的,只要它可以由具有管理员角色的人进行修改。
代币合约需要某种访问控制,因为有些函数(如铸造或对供应参数做任何事情)应该只对被允许的地址可用。最简单的方法是使用Ownable模型(通常使用OpenZeppelin的Ownable合约)。但是使用基于角色的访问控制是有必要的。使用Ownable(或类似的东西)背后的动机可能是简单(和节省gas成本),表面上看这很好。与Ownable模型相比,基于角色的安全性(如OpenZeppelin的IAccessControl)的复杂系数要更高(且昂贵)。如果gas成本仍然是一个问题,我们可以删除基于角色的安全代码,只保留我们需要的内容。但是使用基于角色的安全性的更重要的原因是,它使我们能够将功能(如前面提到的点、销售和定价信息)与ERC721合约本身分离开来。它允许我们通过为其分配“铸造者”角色来指定一个单独的合约作为铸造者,而不允许它拥有完整的管理员权限。而管理员仍然拥有更高级别的权限(例如删除和添加权限)。当铸造者(例如)不再满足我们的需求时,我们只需撤销它的铸造权,并将铸造权分配给一个实现新的铸造策略的新合约就可以;它是模块化的,方便的,安全的。基于项目特定的用例,铸造之外的其他活动也可以以相同的方式进行处理。
许多代币(或一般的合约)要么没有实现ERC-165,要么没有优化地实现它。ERC-165是关于互操作性的。它使我们的合约与未来相兼容,交易所可能会调用它来了解(例如)我们的NFT的版税结构。我经常看到这一点根本没有被执行,或者执行得不是很理想。
以下是正确实现它的法则:
|| 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标准可能会变得更加常用和重要。
我们的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
责任编辑:Felix