Loot一周年回顾:消亡和新
NFT:4 种最常见的 NFT 合约设计缺陷
我们已经见到了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的父类实现处理的接口之外没有实现其他接口,那么就不需要第二种类型。如: