NFT的gas优化终极指南
在我们尝试着创造一个新的收藏品的时候,发现gas费比NFT本身还要贵!
本文旨在解决上面的问题。
接下来我们将看到的是,NFT智能合约的工程团队去寻找降低gas费用的方法时,会发生什么。
本指南是我们精心研究和实验的结果。
在本文中,我们将通过不同的方法来提高铸造的成本效益:
- 我们真的需要ERC721Enumerable吗?
- 使用映射而不是数组
- ERC721A标准
- 从代币 Id 1开始
- 白名单的Merkle树
- 打包变量
- 使用未经检查的
- 为什么第一个铸造更贵,我们能做什么?
- 使用优化器
- 将’ if语句’转换为单独的函数
本文中提到的所有代码都可以在我们的Github上找到:
https://github.com/WallStFam/gas-optimization
我们真的需要ERC721Enumerable吗?
在编写mint函数时,需要确保该函数使用了所需的最少代码。
有时,在合约中添加更多的函数以备将来使用,或者使链下查询更容易。问题是,我们添加的任何额外函数都会增加gas成本。
使用昂贵的Mint函数最常见的情况之一是让我们的合约继承自 ERC721Enumerable。
这个扩展的问题是,它为转账增加了大量的开销。
ERC721Enumerable使用4个映射和一个数组来跟踪每个用户的代币id。在每次转账中写入这些结构要花费大量的gas。
以下是两个智能合约制造一个代币的gas成本的比较。一个继承自ERC721Enumerable,另一个不继承:
ERC721Enumerable的价格是普通ERC721的2倍!
如果我们看看排在第一个铸造之后的铸造,我们就会发现两者的差异更明显:
ERC721Enumerable在第一次铸造后的成本几乎比 ERC721 高出 3 倍!
注意:在solidity中,将变量从0设置为非0比从非0设置为非0更昂贵。这就是为什么在ERC721中的第一个铸造会更昂贵,因为用户的余额从0变化到1。
但有趣的是,ERC721中的第一次铸造更贵,ERC721Enumerable中的第一次铸造更便宜。如果我们有兴趣并且想知道为什么会这样,请查看Open Zepplin的ERC721Enumerable的98行。
第一个铸造是更昂贵的,因为_addtokentownerenumeration(address,uint256)在第一个铸造将映射中的值设置为零,并将值从0设置为0是没有成本。
因此,在添加ERC721Enumerable之前,请自问:“我的合约中真的需要这个函数吗?”
如果我们只打算从合约之外查询每个用户的代币id,那么有一些方法可以做到这一点,而无需使用ERC721Enumerable。
这里有两种方法:
- 为每个代币调用ownerOf(uint tokenId)。
- 查询来自ERC721的Transfer事件并处理它们以获得每个代币的所有者
可以在我们的Github库中找到这两种方法的脚本:
- https://github.com/WallStFam/gas-optimization/blob/master/scripts/getAllOwners/getAllOwners_ownerOf.js
- https://github.com/WallStFam/gas-optimization/blob/master/scripts/getAllOwners/getAllOwners_Transfer_event.js
这些脚本查询区块链以获得ERC721合约的每个代币的所有者。
我们在这些脚本中使用了Wall Street Dads合约作为示例,但是我们可以自由地将该代码用于任何其他合约。我们只需要替换abi和合约地址。
使用映射而不是数组
有时可以用映射替代数组的函数。映射的优点是,我们可以访问任何值,而不必像通常使用数组那样进行迭代。
例如,在NFT集合中使用白名单是很常见的。被添加到白名单的用户有优先权,通常可以得到比公开销售更低的价格。
我们可以做一个白名单使用数组如下:
address[] whitelistedUsers;
function mintPublicSale() external payable {
require(msg.value >= 0.2 ether, "Not enough ether");
_mint(msg.sender, currTokenId++);
}
function mintWhitelist() external payable {
require(isWhitelisted(msg.sender), "You are not whitelisted");
require(msg.value >= 0.1 ether, "Not enough ether");
_mint(msg.sender, currTokenId++);
}
function addToWhitelist(address user) external onlyOwner {
require(!isWhitelisted(user), "User is already whitelisted");
whitelistedUsers.push(user);
}
function isWhitelisted(address _user) public view returns (bool) {
for(uint i=0; i<whitelistedUsers.length; i++){
if(whitelistedUsers[i] == _user){
return true;
}
}
return false;
}
尽管这段代码有用,但它有一个大问题:随着越来越多的用户被添加到whitelistedUsers数组,调用mintWhitelist()的变得越来越昂贵。这是因为数组越大,需要迭代的次数就越多,以确定是否添加了用户。
通常情况下,Solidity中的循环可能不是正确的解决方案。在某些情况下使用数组是可以的,但要确保循环是