5

Most of the ERC721 examples using Open Zeppelin I see require the mint function to have an access control where only the owner of the contract is allowed to call the function. For example,

function mint(address to) public virtual {
    require(hasRole(MINTER_ROLE, _msgSender()), "ERC721PresetMinterPauserAutoId: must have minter role to mint");

    _mint(to, _tokenIdTracker.current());
    _tokenIdTracker.increment();
}

or the following using the Ownable library.

function mint(address receiver) external onlyOwner returns (uint256) {
    _tokenIds.increment();

    uint256 newTokenId = _tokenIds.current();
    _mint(receiver, newTokenId);

    return newTokenId;
}

Does this mean a new contract has to be deployed each time a new token is minted? This seems not only excessive in terms of the gas fee, but also the ERC721 contract has properties for mapping different owners and tokens:

// Mapping from token ID to owner address
mapping (uint256 => address) private _owners;

// Mapping owner address to token count
mapping (address => uint256) private _balances;

which wouldn't make sense if minting is restricted to the contract owner.

It makes more sense to me that you deploy a single ERC721 contract (and its dependencies) and have the users call the mint function. What is the best practice for the mint function of ERC721?

Kevvv
  • 3,655
  • 10
  • 44
  • 90

1 Answers1

5

The ERC-721 standard does not define a "best" or "correct" way to mint new tokens (such as whether it should be open or restricted) and it's up to each contract developer to implement or omit the minting feature in a way that reflects their needs.

Creating of NFTs ("minting") and destruction 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.

But having a whitelist of addresses that are authorized to mint new tokens (e.g. MINTER_ROLE or onlyOwner) seems to be more common than allowing anyone to freely mint new tokens.


Even though it's theoretically possible to deploy new contract each time a new token is minted, it's not a standard approach (and I personally haven't seen any contract that does it). In most cases the minting process "just" creates a new ID, stores a new string/URL value associated with the ID, associates this new token with an owner address (of the token, not a contract owner), plus updates some metadata such as amount of tokens owned by an address (see example below).

The token owner can then transfer their tokens, give anyone control over their tokens, and do other stuff depending on the contract implementation.

The mappings that you point out in your question (_owners and _balances) suggest that they store token owner (not contract owner) addresses as well as amount of tokens held by each address.

Example:

  1. Contract owner mints token ID 1 to address 0x123.

    • Value of _owners[1] is 0x123 (was 0, the default value)

    • Value of _balances[0x123] becomes 1 (was 0, the default value)

  2. Contract owner mints token ID 2 to address 0x123.

    • Value of _owners[1] is still 0x123

    • Value of _owners[2] is now 0x123 (was 0, the default value)

    • Value of _balances[0x123] becomes 2 (because they now own 2 tokens)

Petr Hejda
  • 40,554
  • 8
  • 72
  • 100
  • Are the ERC721 contracts mentioned here only useful if the contract owner is minting tokens for certain addresses? I'm actually looking for a structure where users can mint their own tokens without the central figure. Is there any security flaws if I were to simply remove the access control for the mint function and make it public? I'm just trying to make sense of the reason for the access limitation because I'm not sure if it's as important as the access control for transferring or burning a token. – Kevvv May 19 '21 at 23:30
  • If you want to open the minting feature, you can remove the authorization from the `mint()` function. From security standpoint, I can only think of higher probability of reaching the ID max value (if you chose to make it `uint8` or some "small" datatype) and possible integer overflow (if you don't prevent it either using Solidity 0.8+, or checking with `require`/`assert`). Otherwise, it's same level of security as if you only had authorized addresses, because they should be treated in the code as untrusted as well (e.g. don't trust but verify that they return some value if they're a contract). – Petr Hejda May 20 '21 at 08:21
  • Is it a more common approach to deploy a new contract when the URL is different (to signify a different creature as opposed to just a different variant) or is it better to re-use the old contract and keep adding different URLs for different things to the same contract? – Kernel James Aug 02 '22 at 01:43
  • @KernelJames A general practice is one contract for one collection. So if you have a collection of "cat NFTs", you might want to deploy a new contract for another collection of "dog NFTs". But if your overal aim is to create a collection of "animal NFTs", they you might want to group all these tokens under the one collection. – Petr Hejda Aug 02 '22 at 06:57