Batch minting in constructor with IERC721Enumarable interface

new Implementation of ERC721 that removes minting fee or no transaction fee minting.(ERC721FancyMint.sol)

As of today there is two way to mint ERC721 NFTs:

  1. lazy minting or just in time minting.
  2. pay the minting fees your self and list them.

I created a way to mint arbitrary number of NFT with no minting fee.

how other ERC721 works?

at the heart of any ERC721 there is data storage that hold what token(token Id number) belongs to which Ethereum address.
owner

this is a “Mapping”, Mapping is like a kind of magical table/record, it has a default value for every possible entry.
the default value for addresses are address(0).

as people mint tokens, contract populates the mapping with user address.


An ERC721 contract have to determine which tokens exist.

  • for example in ERC721 Implemented by OpenZeppelin the Rationale behind token existence is if a token owner address is 0x000…0000 (also called address(0)) this token does not exist, if anything else the token is exist and owned by that address.

in the image above only the tokenID 0, 1, 2 are exist.

my Implementation:

This implementation I need to introduce you to 2 new parameter.

  1. MaxSupply: total number of token that we want to mint(it can be any number without increasing transaction fee)
  2. preOwner: the address that we want to mint maxSupply of token to.(maybe the creator)

This Implementation uses a new abstraction for token existence, instead of checking token owner not being address(0) to exist, it check if a token Id is less than MaxSupply it exists, and if the token owner is any address beside address(0) it belong to that address, if the owner was address(0) it belongs to preOwner.

also at deployment make the balance of the user equal to MaxSupply.

In this way preOwner have MaxSupply number of token in their wallet.

Example: If we create a collection with this implementation with the MaxSupply of 999, token 0 to 998 would be exist.


If you want to know more about the Implementation please look at the following links:

  1. Ethereum Magicians:Idea : minting one billion NFTs or 2^256
  2. Ethereum Magicians: EIP Draft : mint arbitrary number of tokens(ERC-721) via EIP-2309
  3. GitHub: EIP-draft_ERC721FancyMint.sol

benefits

  1. no minting fee for any number of token.
  2. tokens are accessible trough all Ethereum platforms at the same time.(like market places, block explorer,…)
  3. users can view the tokens before owning them.

Deployed on the mainnet.

Fancy Project : Premeium

I created a collection around this. it has 999 item and the art is on-chain SVG.
and I list them in different market places. something that you can’t do with marketplaces lazy minting contract.
OpenSea Link


LooksRare Link


and as I promised they all minted with no transaction.

etherscan


manifold example:

they have one of the best implementation of batch minting but because of max gas limit of 12 million or 200 token per transaction.


but this way you can bring down the gas usage to zero for any number of token.

the magic happens all with the help of EIP-2309 the consecutiveTransfer event(event is a thing that notify other application that there is something to check for) instead of simple one by one transfer.
consecutiveTransfer of token 0 - 998, are in the transaction log

PS:
Fancy Project : Premeium is fully compatible with IERC721 and IERC721Enumerable by using ERC721FancyMint and ERC721FancyMintEnumerable (ERC721FancyMIntEnumerable is a bit too long to explain here)
code: Github-Fancy_project_premium

4 Likes

As someone that frequently uses lazy evaluation, I am somewhat shocked that this technique wasn’t already standard practice. Kudos!

1 Like

Thank you for taking the time to read the article and for your good energy.
I proposed this technique back in November but it seems nobody notices it(you are the first)
If you have any recommendations, from who might be interested to see this, or where should present it. I be glad.

OpenZeppelin added ERC721Consecutive extension to OpenZeppelin Contracts which uses ERC2309

4 Likes

Emm,. it still cost gas to change the MaxSupply, right?

1 Like

The technique used above, compared to other methods, costs practically nothing. The gas cost is ~3000 to mint any number of NFTs (even after contract construction).

2 Likes

In the ERC721Consecutive.sol there is_mintConsecutive(address to, uint96 batchSize) for minting, since it uses batch minting, it has O(n) time complexity. I propose we fully omit minting functions.

ERC721Consecutive.sol:

function _ownerOf(uint256 tokenId) internal view virtual override returns (address) {
        address owner = super._ownerOf(tokenId);

        // If token is owned by the core, or beyond consecutive range, return base value
        if (owner != address(0) || tokenId > type(uint96).max) {
            return owner;
        }

        // Otherwise, check the token was not burned, and fetch ownership from the anchors
        // Note: no need for safe cast, we know that tokenId <= type(uint96).max
        return _sequentialBurn.get(tokenId) ? address(0) : address(_sequentialOwnership.lowerLookup(uint96(tokenId)));
    }

ERC721FancyMint(my proposal):

function _ownerOf(uint256 tokenId) internal view virtual returns (address) {
        address owner = _owners[tokenId];
        if (owner == address(0) && (tokenId < _maxSupply)) {
            return _preOwner;
        }
        return owner;
    }

this way we do not need any operation.


Also this abstraction is extendable to IERC721Enumerable with ERC721FancyMintEnumarable.sol that ERC721Consecutive.sol in current implementation can’t be.

function tokenOfOwnerByIndex(address owner, uint256 index)
        public
        view
        virtual
        override
        returns (uint256)
    {
        
        address _preOwner = ERC721FancyMint.preOwner();

        require(
            index < ERC721FancyMint.balanceOf(owner),
            "ERC721Enumerable: owner index out of bounds"
        );
        if (_preOwner == owner) {
            return preIndex(index);\\  a view function that handles preOwner index
        } else {
            return _ownedTokens[owner][index];
        }
    }
function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override {
        super._beforeTokenTransfer(from, to, tokenId);
        address _preOwner = ERC721FancyMint.preOwner();

        if (from == address(0)) {
            //does not support minting
            revert("fromm == zero, does not support minting");
        } else if (from != to) {
            if (from == _preOwner) {
                _removeTokenFromPreOwner(tokenId);
            } else {
                _removeTokenFromOwnerEnumeration(from, tokenId);
            }
        }
        if (to == address(0)) {
            //does not support burning
            revert("to == zero, does not support burning");
        } else if (to != from) {
            if (to == _preOwner) {
                _addTokenToPreOwner(tokenId);
            } else {
                _addTokenToOwnerEnumeration(to, tokenId);
            }
        }
    }

I rather don’t change the maxSupply , and preOwner.
and this implementation is more fit toward fixed length collections.

constructor(
        string memory name_,
        string memory symbol_,
        uint256 maxSupply_,
        address preOwner_
    ) {

        require(preOwner_ != address(0), "preOwner can NOT be address(0)");
        require(0 < maxSupply_, "maxSupply_ should not be zero!");
        _name = name_;
        _symbol = symbol_;

        //@dev my proposal

        _maxSupply = maxSupply_;
        _preOwner = preOwner_;
        // blance of preOwner
        _balances[preOwner_] = maxSupply_;

        // see eip-2309 examples for batch creation
        emit ConsecutiveTransfer(0, maxSupply_ - 1, address(0), preOwner_);
    }

Can you share with us a TX link on etherscan that actually yields zero gas cost?

the zero gas cost is because there is no minting function.
if you look at EIP-721 there is no mint function there:

Creation of NFTs (“minting”) and destruction of NFTs (“burning”) is not included in the specification. Your contract may implement these by other means. Please see the event documentation for your responsibilities when creating or destroying NFTs.

and “event” is:

/// @dev This emits when ownership of any NFT changes by any mechanism.
   ///  This event emits when NFTs are created (`from` == 0) and destroyed
   ///  (`to` == 0). Exception: during contract creation, any number of NFTs
   ///  may be created and assigned without emitting Transfer. At the time of
   ///  any transfer, the approved address for that NFT (if any) is reset to none.
   event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

and EIP-2309 introduced a new event that make batch token creation more efficiently.

Batch token creation
emit ConsecutiveTransfer(1, 100000, address(0), toAddress);

In my implementation, I simply use no operation or state change. I only notify applications, with an consecutiveTransfer event:
emit ConsecutiveTransfer(0, maxSupply_ - 1, address(0), preOwner_);
and it’s done in the constructor at deployment.

please see the transaction log.
deployment tx:
https://etherscan.io/tx/0x59537f3f817a7e621462afe16e6435032e9c63a66169a031e559af671363a6f2#eventlog

If what you propose is “mint during contract creation and using 2309 to do it ‘cheaper’ for large amount or use a fallback owner, and outside of constructor I don’t do minting so I don’t pay for TX I didn’t incur” then ok by but that’s very limited in usebility for any real use case I am afraid. It doesn’t create any benefit avoiding gas cost IMHO

There title of this thread might better update to “mint only during constructor plus a default owner saves some gas if you don’t mint at all after contract creation.”, I guess?

Mint by its definition involves creation of token and can happen in any stage of a contract. Combining the minting process with constructor doesn’t actually make the cost zero, it only bundles them together. Reading the title I was anticipating some other magic moves like gas banking or return of gas from existing balance of a contract.

3 Likes

about the title you are right, I have posted this idea with different titles but they couldn’t spark the conversation. hence “0 gwei”

the thing that I’m trying to address here is aimed for collections that has maxSupply and their minting function become deactivated after minting period. In my observation respectable portion of NFT ecosystem are these collection.

I think the part that this implementation become useful is:

  1. when people want to lazy mint NFT their collection is only available in one market place.

  2. If they decided to use ERC721Consecutive.sol they loose the lack of IERC721Enumarable compatibility.

I really enjoyed the new abstraction of _ownerOf in ERC721Consecutive.sol and I changed it a little.
since some Dapps are dependent on tokenOfOwnerByIndex(address owner, uint256 index),It’s possible to use ERC721FancyMintEnumerable extension,` I think this Implementation would be useful in some cases

but they couldn’t spark the conversation. hence “0 gwei”

I couldn’t believe, click-bating, this evil thing, is now in the sacred land of Ethereum-Magician, how dare you, my friend! @sciNFTist.eth
(just kidding. well-played)

2 Likes

Actually, they’re not wrong in saying that it’s gasless minting. The marginal cost of minting an NFT is exactly zero. The fixed cost is (roughly from my estimates) 3000.

1 Like

seems similar to https://erc721a.org

2 Likes

there is a pros and cons between these two implementation:

  1. after deployment ECR721A maintain the mint & batch minting functionality that ERC721FancyMint does not.
  2. ERC721FancyMint has an extension ERC721FancyMintEnumarabe that give the contract full compatibility with IERC721Enumarable interface, but ERC721A does not have IERC721Enumerable interface.

PS: ERC721A has an ERC721AQueryable.sol extension that has an interface like this:

 /**
     * @dev Returns an array of token IDs owned by `owner`.
     *
     * This function scans the ownership mapping and is O(`totalSupply`) in complexity.
     * It is meant to be called off-chain.
     *
     * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into
     * multiple smaller scans if the collection is large enough to cause
     * an out-of-gas error (10K collections should be fine).
     */
    function tokensOfOwner(address owner) external view virtual override returns (uint256[] memory) {}

and another interface:

/**
     * @dev Returns an array of token IDs owned by `owner`,
     * in the range [`start`, `stop`)
     * (i.e. `start <= tokenId < stop`).
     *
     * This function allows for tokens to be queried if the collection
     * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}.
     *
     * Requirements:
     *
     * - `start < stop`
     */
    function tokensOfOwnerIn(
        address owner,
        uint256 start,
        uint256 stop
    ) external view virtual override returns (uint256[] memory) {}

and it has time complexity of O(totalSupply) but tokenOfOwnerByIndex(address owner, uint256 index) in ERC721FancyMintEnumarable has O(1) time complexity.

and as I promised they all minted with no transaction

this is of course nonsense, but for anyone else who might be confused the minting happens in the deploy transaction

2 Likes

yeah your right, the title was confusing so I’ve changed it,
thank you for pointing that out.
but the main focus of this implementation. is batch minting and while maintaining enumerability with complexity of O(1). mainly tokenOfOwnerByIndex(address owner, uint256 index) from ERC721Enumerable.
a quick comparison:
openZeppelin :ERC721Consecutive.sol

abstract contract ERC721Consecutive is IERC2309, ERC721 

chiru-lab: ERC721AQueryable.sol

abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable

ERC721FancyMintEnumerable

contract ERC721FancyMintEnum is ERC721FancyMint, IERC721Enumerable

If you could provide your feedback on ERC721FancyMintEnumerable, I’ll be glad.

PS: ERC721AQueryable has an interface like function tokensOfOwner(address owner) this that has O(collectionSize) time complexity.

 /**
     * @dev Returns an array of token IDs owned by `owner`.
     *
     * This function scans the ownership mapping and is O(`totalSupply`) in complexity.
     * It is meant to be called off-chain.
     *
     * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into
     * multiple smaller scans if the collection is large enough to cause
     * an out-of-gas error (10K collections should be fine).
     */
    function tokensOfOwner(address owner) external view returns (uint256[] memory);
}

thank you for reading this.

thanks for updating the title of your post, it’s more factual now

3 Likes