ERC721 Smart Contracts in 2022

Overview:

NFT smart contracts have come a long way from Cryptopunks and the days of yore. With the huge boom in popularity of NFTs over the past year, EIP1559, and the rising gas costs of Ethereum, Developers have implemented many optimization strategies to lower minting, deployment, and other costs in general. I will cover some of the notable methods to optimize your ERC721 contracts. Most of these tips can be applied to other smart contracts as well.

Keep in mind, not all of these methods are ‘new’, but they are (sadly) not widely used and should be adopted in place of less efficient patterns. If you know of any patterns that you would like to add to this list, please let me know over on Twitter @dd0sxx!

Useful Patterns and Tips for Devs:

ERC721A:

The Azuki team developed a very clever successor to the ERC721.sol contract from OpenZeppelin, aptly named ERC721A.sol.

You can read more detail about the optimizations here, but the TLDR is that they reduced the cost for batch minting to be almost equal to that of a single mint. They accomplished this by not explicitly writing ownership to storage for every token (after the first one) in a loop, and modifying the ownerOf function to check tokenIds lower than the one input for the owner.

ERC721 + Counters > ERC721Enumerable:

ERC721Enumerable.sol is a popular contract in the OpenZeppelin library. It is an extension of their standard ERC721.sol contract. The ERC721Enumerable interface has three distinct functions: totalSupply, tokenOfOwnerByIndex, and tokenByIndex. The one we care about is totalSupply - this function returns the current number of NFTs that have been minted to the contract so far. This is typically used in the mint function to determine the tokenID to mint next, like this:

Contract OldEnumerable is ERC721Enumerable {
	function mint () public {
		uint256 mintIndex = totalSupply() + 1;
		_safeMint( msg.sender, mintIndex );
}

We can replace this function with the Counters.sol Library from OpenZepelin to save gas costs on mint.

Contract NewCounters is ERC721Enumerable {
	using Counters for Counters.Counter;
	Counters.Counter private _tokenSupply;
	function mint () public {
		uint256 mintIndex = _tokenSupply.current() + 1;
		_safeMint( msg.sender, mintIndex );
		_tokenSupply.increment();
}```

The savings come from the fact that in the Enumerable contract, there is a very expensive _beforeTokenTransfer function that gets run on every _mint and _transfer call. This function performs many expensive storage operations (updating multiple mappings and arrays per call), whereas the counter method only performs a single storage manipulation on a primitive once.

Read more in-depth about the gas savings and implementation here

Pre-Approving Opensea and Other Marketplace Contracts:

By overriding the isApprovedForAll function in our ERC721 contracts, we can pre-approve specific contracts to save our users gas and a transaction. By doing this, users can list their NFTs for sale for free and improve the UX 😄

Here is an example code snippet that pre-approves the Opensea proxy contract (not production ready or audited)

function isApprovedForAll( address owner, address operator) public view override returns (bool) {
    return (operator == 0x7Be8076f4EA4A4AD08075C2508e481d6C946D12b) ? 
    true 
    : 
    super.isApprovedForAll(owner, operator);
}

one could easily replace this singular address with an array to add other marketplaces, such as Looksrare. Find a more full implementation here:

EIP2981: NFT Royalty Standard:

EIP2981 (read more here) is an NFT royalty standard. It has not been implemented in all marketplaces yet, but is absolutely the de facto way to document secondary royalty sales - where they should go and how much.

Previously, each marketplace had their own unique royalty systems that did not cross over between platforms. This means if an artist did not set their royalties on each site, they could be losing out on any transfer that happen there. This is what EIP2981 aims to solve so it is important that we as developers implement it into our own work whenever possible.

Here is what this looks like in code:

function royaltyInfo(
        uint256 _tokenId,
        uint256 _salePrice
    ) external view returns (
        address receiver,
        uint256 royaltyAmount
    );

Don’t forget to integrate this with EIP165 for standard interface detection!

Merkle-Drop your Whitelist:

Do you have a list of users you want to airdrop or give minting privileges to?

Undoubtedly, a Merkle-Drop is the most efficient way to execute it. Instead of writing an expensive array of addresses to your contract, just write a single bytes32 Merkle-Proof.

Read an Open Zeppelin article on Merkle-Drops to learn more and check out this ERC721MerkleDrop contract.

Compiler Optimization:

This should come as no surprise to developers, but there are a lot of deployment cost savings to be had with this simple adjustment. Add these settings to your config and let the compiler do its magic: (example is from a hardhat.config.js file, but the same will work in any environment)

solidity: {
    version: "0.8.11",
    settings: {
      optimizer: {
        enabled: true,
        runs: 1000,
      },
    },
  }

I tested one of my contracts deployment gas costs with and without this optimization, and running the optimizer saved me slightly over 28%! Huge upside with extremely minimal effort.

Manifold’s use of Proxy Contracts:

I plan to write more in-depth about the Manifold Creator Studio contracts, but what the devs at Manifold did here is very special and something I hope to see more of in the space moving forward.

Manifold deployed a proxy contract that holds all of the logic for ERC721s & ERC1155, and allows creators to deploy contracts for extremely cheap. Why are they so cheap? Well, they have almost no code and delegatecall for everything to the logic proxy contract. This enables creators to deploy their own contracts for a fraction of the price than they previously could, all with a no-code platform that has support for 3rd party plugins that enable features like lazy minting, and more.

Looking forward to seeing more use of proxy patterns in the NFT space, as I have noticed they are commonly used in DeFi contracts and elsewhere.

Check out the contracts here:

An example contract deployed from the proxy (this is a project from an artist that my NFT studio helped bring to fruition)

That’s all for this round. Thanks for reading, and If you want to add something to this list that you think other devs should know, please reach me on Twitter @dd0sxx so we can make it happen!

Subscribe to dd0sxx
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.