npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@beskay/erc721b

v1.0.4

Published

Updated, minimalist and gas efficient version of OpenZeppelins ERC721 contract

Downloads

13

Readme

ERC721B

A fully compliant implementation of IERC721 with significant gas savings for minting multiple NFTs in a single transaction. Includes the Metadata and Enumerable extension.

The ERC721B standard builds on top of ERC721A from Azuki, reducing the minting fees by around ~17k gas on average and the transfer fees by around ~7k gas.

The table below shows a comparison of gas costs between a standard ERC721Enumerable contract, an ERC721A contract and this ERC721B contract. The two columns on the right show the gas savings compared to an ERC721Enumerable contract and compared to an ERC721A contract.

Table of gas savings

How it works

Removed OpenZeppelins Enumerable implementation

Out of the box, Open Zeppelin ERC721Enumerable comes with an inordinate amount of transfer processing that simply is not needed for the majority of projects. With ERC721B, the method of tracking is moved into view functions, this saves an huge amount of gas when minting or transferring your tokens. For more info on that, I highly recommend reading this medium article from nftchance.

Included optimizations from ERC721A

I included the optimizations from Azukis ERC721A contract, namely updating the owners balance only once per batch mint request, instead of per minted NFT. Thanks to this, minting is cheap -- no matter how many NFTs you mint at once. For more info, see Azukis blog post

Using an array as owner storage

This is actually the "secret ingredient", the reason why this implementation is even more gas efficient than ERC721A. Instead of using mappings to store the owner data, we use an array. So

struct AddressData {
    uint128 balance;
    uint128 numberMinted;
}
// Mapping owner address to address data
mapping(address => AddressData) private _addressData;

uint256 internal currentIndex = 0;

Becomes

// Array which maps token ID to address (index is tokenID)
address[] internal _owners;

The balance variable is substituded with a balanceOf() function call, numberMinted is removed, currentIndex can be replaced with _owners.length. This saves us a few storage writes and therefore some gas.

Following storage writes in the mint function of ERC721A

_addressData[to].balance += uint128(quantity);
_addressData[to].numberMinted += uint128(quantity);

_ownerships[startTokenId].addr = to;
_ownerships[startTokenId].startTimestamp = uint64(block.timestamp);

...

currentIndex = updatedIndex;

are substituded with

_owners.push(to);

In the image below you can see an example layout of the _owners array:

owners array

In this example wallet 0x1234...6789 minted 3 tokens and wallet 0x4567...8745 minted 4 tokens (0x9876...1234 minted an unknown number of tokens since the previous owner isnt shown).

As you can see, an owner is only set for the last minted tokenId, the previous ones keep their default value. Over time (after every token got transferred at least once) all indices will be set to a specific owner.

_checkOnERC721Received

Unlike in the standard ERC721 implementation this is only called once per batch mint. Calling this several times per batch mint is a waste of gas, if the contract confirms the receival of one token, it will accept all additional tokens too. This saves us around 5k gas per additional mint, so it adds up quite a bit.

Please note that this is an experimental feature, it could be that there are some contracts out there which use the onERC721Received function for additional logic, like sending the received NFTs to another wallet or something else, I am not aware of any though.

Installation

npm install --save-dev @beskay/erc721b

How to use

Once installed simply import the contract and inherit from it.

pragma solidity ^0.8.4;

import '@beskay/erc721b/contracts/ERC721B.sol';

contract Example is ERC721B {
  constructor() ERC721B('Example', 'EXMP') {}

  function mint(uint256 quantity) external payable {
    // _safeMint's second argument now takes in a quantity, not a tokenId.
    _safeMint(msg.sender, quantity);
  }
}

You can also take a look at ERC721BMock.sol

Recommendations

Keep max batch size limit low

Even if minting multiple tokens at once is cheap, dont set the max batch limit too high. The higher the max batch limit, the higher are the gas costs of subsequent transfers -- on average. This is due to the ownerOf() function, which iterates over the _owners array until it finds a nonzero element, so gas spent here starts off proportional to the maximum mint batch size. It gradually moves to O(1) as tokens get transferred around in the collection over time.

Dont call balanceOf() and tokenOfOwnerByIndex() on chain

The gas savings by using an _owners array instead of a mapping comes at a cost: The balanceOf() and tokenOfOwnerByIndex() are highly inefficient. They iterate over the complete array, this means if a collection has 10000 NFTs in total, they will iterate over 10k items.

Because of this, calling these functions from another smart contract can become extremely expensive, e.g. calling the balanceOf() function from a 10k NFT project costs around 22 million (!) gas, 2/3 of the block gas limit.

Fortunately, calling those two functions from another smart contract is almost never needed and if it is, you can probably substitute the call off chain: Usually you call balanceOf() to check if someone holds NFTs from a specific project in order to whitelist them (or something similiar). Instead of calling balanceOf() from your smart contract, you can check the tokenIds a wallet holds by calling tokenOfOwnerByIndex() off chain, and then prove it on chain by calling ownerOf(tokenId).

Safety

This is experimental software and is provided on an "as is" and "as available" basis. This contract is not audited yet.

It was not designed with user safety in mind. You should thoroughly read the contract before using it for your own project.

I do not give any warranties and will not be liable for any loss incurred through any use of this codebase.

FAQ

How does it compare to ERC721A?

ERC721A is designed with user safety in mind, ERC721B is designed for maximum gas savings. Its a "lightweight" ERC721A contract, so to say. This contract is still absolutely fine and safe to use, just remember that calling balanceOf() and tokenOfOwnerByIndex() on chain should be avoided.

How can i save even more gas?

If you dont want to support minting to smart contracts, you can use the mint function instead of safeMint. This saves you another ~8k gas, since _checkOnERC721Received wont be called. Make sure that smart contracts wont be able to mint to prevent loss of NFTs, e.g. by using a whitelist.

Why is the contract marked as abstract?

The contract is marked as abstract because the tokenURI function is not implemented.

Acknowledgements

These contracts were inspired by or directly modified from many sources, primarily:

A big influence was also this medium article from nftchance

Contact