NFT 繁荣已经发生,并且仍在发生(截至 2022 年 7 月撰写本文时)。 Etherscan 有一个方便的搜索实用程序,连同其方便的验证和反编译功能,让您可以查看许多 ERC721 的代码以进行比较。除了许多精心设计的合约外,我们还可以看到许多人一遍又一遍地犯同样的错误。在本文中,我将就我认为 NFT 最常见的 4 种“设计失败”发表我的看法,这是我在 etherscan 上查看 NFT 合约时通常会注意到的。
请注意,本文主要是针对与 EVM 兼容的区块链编写的,但其中许多观点在其他网络上也适用或有一些类比或等价物。
如果你犯了我所说的“设计失败”,请不要受到侮辱。这些是我的意见,此外,作为一名开发人员,我完全理解在这些 gas 昂贵的网络拥塞时期节省 gas 成本的必要性。考虑一下我作为自由顾问的观点;一个可以花费数千美元用于开发帮助的客户,为了追求卓越,肯定可以多花一百美元进行部署。
这当然是指以太坊链,这是撰写本文时最昂贵的; Polygon 就不那么重要了,像 Solana(一个非 EVM)这样的其他链就更不重要了。我的观点是,如果有资金,更高质量的实施所带来的好处可能值得额外的成本。
这是非常常见的,但是当我看到这个时,它会在我眼中将合同标记为业余行为。公平地说,存在有效且可以理解的动机。一方面,在许多网络上部署和管理合约变得非常昂贵,并且已经努力节省这些成本。而且,为了简单起见,有人可能会想,为什么不将铸币和销售逻辑放入合约本身呢?
但这并不是一个好主意。合约本身应该是逻辑网络的不可变中心,但绝不应该直接处理金钱而弄脏自己的手。这包括销售、销售时间、白名单等,直接在与 ERC721 实现相同的合同代码中。销售逻辑和核心逻辑是紧密耦合的。
虽然节省 gas 成本可能是将所有逻辑塞进一份合约的最佳和最容易理解的理由,但我认为,考虑到所有因素,有更好的理由不实施这种设计捷径。您的核心合约逻辑应该是唯一一成不变的,并且在大多数情况下将以非常、非常……标准的方式实施标准。许多克隆是(或可能)几乎是彼此的克隆。你的铸币策略、定价(如果你在卖铸币)——这些东西应该是分离的。这允许您的合同以不损害用户信任的方式灵活。解耦设计和单一职责原则。旁注:我认为在 ERC721 合约本身中限制供应(即 maxSupply)确实有意义,只要它可以由具有管理员角色的人修改。
代币合约需要某种访问控制,因为有些功能(例如铸造或对供应参数做任何事情)应该只对许可地址可用。实现这一点的最简单方法是使用 Ownable 模型(通常使用 OpenZeppelin 的 Ownable 合约,因为为什么要为这样的基本需求重新发明轮子)。但我强烈建议改用基于角色的访问控制,原因如下。使用 Ownable(或类似的东西)背后的动机可能是简单(并节省 gas 成本),这在表面上很好。您可能还“知道”您(或您的客户)将“始终”是唯一管理合同的人。当成本低时,面向未来是可取的;与 Ownable 模型相比,基于角色的安全性(例如 OpenZeppelin 的 IAccessControl)的复杂性实际上只是稍微复杂一些(而且成本更高)。如果 gas 成本仍然是一个问题,您总是可以将基于角色的安全代码(无论是 OpenZeppelin 还是您自己的)修剪为您需要的。但是使用基于角色的更重要的原因是它使您能够将功能(如前一点,销售和定价信息)与 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 令牌可能非常标准,并且可以使用所有第三方父类和库,只需很少的自定义,您可能知道第三方代码经过良好测试和安全而著称。但是你仍然需要彻底测试你的代码,因为在它被部署到主网之前你只有一次机会得到它,给你带来永远的荣耀或耻辱。
首先,当然是单元测试。在我看来,你使用什么测试框架并不重要;我将安全帽与醚和摩卡一起使用。对我来说,唯一重要的部分是测试覆盖率和快乐路径案例、异常案例和边缘案例的覆盖范围广泛而深入。即使您可能正在测试已经众所周知的经过良好测试的代码(例如 OpenZeppelin),(a)您的自定义代码可能已经破坏了其中一些情况,因此应该重新测试它们,并且(b)OpenZeppelin 之前有过错误,并且他们将来可能会再次出现。为了节省一些时间,您可能有一套标准测试套件,适用于所有 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
所以,总结一下:
在 etherscan 上验证合约代码是一个很棒的功能。看到“合同”选项卡上的绿色复选标记,并能够看到带有“完全匹配”标签的代码本身,就体现了信任和责任感。当有人第一次查看您的合同,试图评估使用或投资的相对风险时,这只会有所帮助。这通常适用于所有类型的所有合约,而不仅仅是 NFT。