Create a NFT collection smart contract


What is technically a NFT ?

A NFT is a token, belonging to a collection, where each token in this collection is unique. Each token is identified by an id, and belongs to an owner (an Ethereum account address).

Some data can be attached to this token, like a URI.

Usually, the URI points to a metadata file, that contains information about an asset, including the URI to this asset.

It is recommended to host the asset and the metadata file on a decentralized file system like IPFS.

Here is the metadata JSON schema of the ERC 721 standard.

{
  "title": "Asset Metadata",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Identifies the asset to which this NFT represents"
    },
    "description": {
      "type": "string",
      "description": "Describes the asset to which this NFT represents"
    },
    "image": {
      "type": "string",
      "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
    }
  }
}

There exists other schemas, adding other attributes to this one, like OpenSea’s schema.

Here is a schema to visualise the metadata and asset URIs

NFT standard

The standard ERC 721 defines how we should implement a NFT smart contract.

It only defines the interface (which method we should implements), but not the implementation. Instead, of doing our own implementation, we should better use OpenZeppelin’s:

  • it’s less work for us
  • it’s safer. Once the contract is deployed, it’s impossible to update it. And Open Zeppelin is focused on security

OpenZeppelin provide contracts that we can import and inherit from.

Our contract must at least inherit from ERC 721. But depending of the needs, we can inherit some extensions.

In our case:

  • ERC721Enumerable to be able to list the tokens
  • ERC721URIStorage to store a URI with each token. The URI points to something off chain, but the association between the token and the URI is stored on chain.

Implementation of the smart contract

If we look at OpenZeppelin ERC721 contract, we can see that there is a _safeMint method, but it’s an internal method. So we will have to create a public method to mint a token. This method will take the token owner address and the metadata URI as parameters.

Inside, we will call _safeMint to mint the new token, and call _setTokenURI (ERC721URIStorage) to store the metadata URI.

Also, there is no method to list all the tokens, we can only get a token if we know his id.

So the trick is to use a counter. When the mint method is called:

  • the id of the new token is the counter value
  • the counter is incremented

As we can retrieve the number of tokens minted, if we call this number n, then we know the ids are 0 to n-1.

Here is our smart contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract TestToken is ERC721, ERC721Enumerable, ERC721URIStorage {
    using Counters for Counters.Counter;

		// -> the counter for the ids
    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("TestToken", "TTK") {}

		// -> the public method to mint a token
    function safeMint(address to, string memory uri) public {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }
}

But we are not done yet, almost !

As it’s our own collection, we want to be the only one who can mint new tokens. It can easily be done using the modifier onlyOwner from import "@openzeppelin/contracts/access/Ownable.sol";

Also, if we try to compile like this, we will get errors, because of the polymorphism.

We inherit the methods _beforeTokenTransfer, _burn, tokenURI and supportsInterface from more than one contract. In this case, we must specify explicitly from which contracts we inherit these methods using override.

Here is the final code for our smart contract, ready to be compiled and deployed ! (It can be done with Remix or Hardhat).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract TestToken is ERC721, ERC721Enumerable, ERC721URIStorage {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("TestToken", "TTK") {}

    function safeMint(address to, string memory uri) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }

    // The following functions are overrides required by Solidity.

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId,
        uint256 batchSize
    ) internal override(ERC721, ERC721Enumerable) {
        super._beforeTokenTransfer(from, to, tokenId, batchSize);
    }

    function _burn(
        uint256 tokenId
    ) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }

    function tokenURI(
        uint256 tokenId
    ) public view override(ERC721, ERC721URIStorage) returns (string memory) {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(
        bytes4 interfaceId
    ) public view override(ERC721, ERC721Enumerable) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}

Playing with the contract

Let’s mint some tokens

  • import an image on IPFS, and copy the URI
  • create the metadata file
  • upload the metadata file, and copy the URI
  • call safeMint: safeMint(YOUR_ADDRESS, METADATA_IPFS_URI)

A token has been minted ! Do it as many time as you want 🙂

List the tokens

Get the number of tokens by calling totalSupply()

Then, for each id from 0 to total supply:

  • call tokenURI(TOKEN_ID) to get the metadata file URI
  • call ownerOf(TOKEN_ID) to know the current owner

Transfer the token

To transfer the token to another address, the owner of the token can call safeTransferFrom(CURRENT_OWNER_ADDRESS, NEW_OWNER_ADDRESS, TOKEN_ID).

But if we want to sell it, it gets complicated !

  • The new owner have to transfer some money to us
  • We have to transfer the token to him

How to be sure nobody gets ripped off ?

We need another contract to handle the purchase: a marketplace contract. This is the subject of the next article 😉.