How to write NFT smart contract in 2023
Tags: blockchain, tutorial, beginners, solidity
Published: 2023-01-18 08:53:29


![Banner](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fbj2m00wgwdgs9wpg1uu.png)

## Hello you fellow `coders`!

With the recent craze on NFT space, i ponder myself on how would one build an NFT smart contract. As with any emerging technology buzzword **blockchain**, businesses building NFT projects now will have the advantage of early adopters. 

NFTs allow businesses to connect with their audience in new ways by providing exclusive value and experiences they wouldn't have been able to provide before.

Here i will guide through on how i build a somewhat a mid/advanced NFT smart contract.


![nft smart contract todo list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fdjsewslkfv320t1r8wd.png)

In this project, here are the list of features which is common and the new OpenSea's On-Chain Enforcement Tool [(operator-filter)](https://github.com/ProjectOpenSea/operator-filter-registry) 

**- Batch Minting (ERC721a)**
Allowing user to mint multiple tokens within a single call.

**- Whitelist / Allowlist**
Using [MerkleProof](https://medium.com/crypto-0-nite/merkle-proofs-explained-6dd429623dc5) to validate user address if they are white-listed.

**- Pre-Reveal / Reveal**
Pre-reveal placeholders are used for new generative projects. The artwork won’t actually be revealed until the NFT is minted, and collectors will only know which NFT in the collection they own once they buy the NFT.

**- OpenSea's On-Chain Royalties Enforcement Tool (operator-filter)**
A smart contract that enforce creator royalties.

___

## Initialise project

![lets gooooo](https://media.giphy.com/media/2uIlaHVsql55CLP3as/giphy.gif)

In this tutorial, i will be using hardhat for smart contract development. Hardhat is a development environment for Ethereum software in a nutshell. You can even write test cases against your contract to ensure there no security issue, since once contract is deployed is immutable on the blockchain!

Lets start with creating a directory and installing hardhat as a dev dependency and initialise hardhat.

```
$ mkdir nft-smart-contract && cd nft-smart-contract && yarn add -D hardhat && npx hardhat
```

Follow the steps given by hardhat, in my project i've selected typescript. This will then generates the necessary folder for you to start building your smart contract.


![filder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8h84b66k9q0xb9eisnu9.png)

___

## Next installing dependency

```
$ yarn add -D erc721a @openzeppelin/contracts
```

As you can see we are installing **erc721a** an implementation of ERC721 for batch-minting and reduces gas fees during NFT Batch minting, credits to the [@cygaar](https://twitter.com/0xCygaar) Azuki Dev. And also [@openzeppelin](https://www.openzeppelin.com/) which provides an open-source framework to build secure smart contracts.

___

## Create our first NFT contract!

In the contracts folder create a new `NftSample.sol` file or name it to your choosing.


![example image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k846kz264lmjxus1sgne.png)



In the example here, we're importing and inheriting **ERC721A** and **Ownable**.

**ERC721A** allow us to inherit their batch-minting function later when we modify our `mint()` function

**Ownable** which provides a basic access control mechanism, where there is an account (an owner) that can be granted exclusive access to specific functions. This module is used through inheritance.
___

## Minting (Public)

![minting](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pvwzc0yvsujuw5xkjv5f.png)

Next we will need to create our `mint()` function to allow user to mint multiple tokens. We also will set our token supply to 10k. 

Here are some of the requirements we will be adding.

- check if minting it available base on phases.
- max limit per user address.
- check if enough tokens left
- check if the value is correct

```
   function mint(uint256 quantity) external payable {
        // check if public sale is live
        if (mintPhase != 2) revert PublicSaleNotLive();

        // check if enough token balance
        if (totalSupply() + quantity > maxSupply) {
            revert NotEnoughTokensLeft();
        }

        // check for the value user pass is equal to the quantity and the mintRate
        if (mintRate * quantity != msg.value) {
            revert WrongEther();
        }

        // check for user mint limit
        if (quantity + usedAddresses[msg.sender] > maxMints) {
            revert ExceededLimit();
        }

        usedAddresses[msg.sender] += quantity;
        _mint(msg.sender, quantity);
    }
```

In the example above, we add a check if user are able to mint by setting a phase. We then check if theres enough token available to mint. To ensure user does not pass the wrong value, we've added a check and throw an error if so. Then we do a check if user has already minted the limit allowed by add mapping the user address and the quantity they have minted.

To allow user to batch-mint we inherit `_mint()` that derived from ERC721a and than we set the quantity user minted in a mapping function.

___

## White list mint

![wl mint](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jt9c05wonl6zk1zmnc3f.png)

For our whitelist mint, there are couple of requirements to ensure of whitelist addresses are allowed to call this function. We will be using MerkleProof to verify user address. 

```javascript
    /**
     * @dev a function that only allow whitelisted addresses to mint
     */
    function whiteListMint(uint256 quantity, bytes32[] calldata proof)
        external
        payable
    {
        // check if white list sale is live
        if (mintPhase != 1) revert WhitelistNotLive();

        // check if the user is white listed.
        if (!isWhiteListed(msg.sender, proof)) revert InvalidMerkle();

        // check if enough token balance
        if (totalSupply() + quantity > maxSupply) {
            revert NotEnoughTokensLeft();
        }

        // check for the value user pass is equal to the quantity and the mintRate
        if (whiteListRate * quantity != msg.value) {
            revert WrongEther();
        }

        //check if user exceeded mint limit
        if (whiteListUsedAddresses[msg.sender] + quantity > whiteListMaxMints) {
            revert ExceededLimit();
        }

        _mint(msg.sender, quantity);
        // storing the number of minted items
        whiteListUsedAddresses[msg.sender] += quantity;
    }

    /**
     * @dev a function that check for user address and verify its proof
     */
    function isWhiteListed(address _account, bytes32[] calldata _proof)
        internal
        view
        returns (bool)
    {
        return _verify(leaf(_account), _proof);
    }

    function leaf(address _account) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(_account));
    }

    function _verify(bytes32 _leaf, bytes32[] memory _proof)
        internal
        view
        returns (bool)
    {
        return MerkleProof.verify(_proof, merkleRoot, _leaf);
    }

```

As you can see in the example `whiteListMint` write function, there's 2 arguments, `quantity` & `proof`. 
- `quantity` - the number of tokens user would like to mint
- `proof` - an array of hex strings which is required to verify with `merkleRootHash` that we will be adding later on.

We're adding `isWhiteListed` get function to check and return a `boolean` which uses `MerkleProof.verify(_proof, merkleRoot, _leaf)` which we inherit from `@openzeppelin/contracts/utils/cryptography/MerkleProof.sol` for verification.

In the nutshell, Merkle proof confirms specific transactions/values represented by a leaf or branch hash within a Merkle hash root, verify it and return a boolean.

Here the list of check for our, whiteListMint
- check if whitelistmint is live.
- check if user is whitelisted.
- check the there token left to be minted.
- check if the value send if correct for minting
- check if user exceeded white list mint limit.

___

## Merkle Root Hash


![merkletree](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g8zkawj2st0sn1syw7ks.png)



In order to make whitelisting of addresses works, we need to generate merkle root hash which is a hex string and store in our contract. We will be adding a utils/scripts to do just that, in the example i'am using typescript, but you can use which ever programming language your prefer.

We will be installing the dependencies.

```shell
$ yarn add -D keccak265 merkletreejs
```

```typescript
// utils/merkle.ts

import { ethers } from "ethers";
import keccak256 from "keccak256";
import { MerkleTree } from "merkletreejs";

/**
 * This is to ensure the addresses is valid and injects the checksum
 */
export const getAndConvertAddresses = (addresses: string[]): string[] => {
  if (!addresses?.length) return [];

  const convertedAddress = addresses.map((addr) => {
    return ethers.utils.getAddress(addr);
  });

  return convertedAddress;
};

const generateMerkle = (addresses: string[]) => {
  if (!addresses?.length) return null;

  // This is to ensure the addresses is valid and injects the checksum
  const convertedAddresses = getAndConvertAddresses(addresses);

  /**
   * Create a new array of `leafNodes` by hashing all indexes of the `whitelistAddresses`
   * using `keccak256`. Then creates a Merkle Tree object using keccak256 as the algorithm.
   * The leaves, merkleTree, and rootHas are all PRE-DETERMINED prior to whitelist claim
   */
  const leafNodes = convertedAddresses.map((addr) => keccak256(addr));

  const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true });
  const merkleRootHash = merkleTree.getRoot().toString("hex");
  const merkleTreeString = merkleTree.toString();

  return {
    merkleTree,
    merkleRootHash: `0x${merkleRootHash}`,
    merkleTreeString,
  };
};

/**
 * A function that generates merkle proof hex string
 * @param whitelistAddresses | string []
 * @param walletAddress | string
 * @returns string[]
 */
const getMerkleProof = (
  whitelistAddresses: string[],
  walletAddress: string
) => {
  const convertedAddresses = getAndConvertAddresses([walletAddress]);
  const merkle = generateMerkle(whitelistAddresses);
  const hashAddress = keccak256(convertedAddresses[0]);
  const proof = merkle?.merkleTree.getHexProof(hashAddress);
  return proof || [];
};

const isWhiteList = (
  whitelistAddresses: string[],
  walletAddress: string
): boolean => {
  const convertedAddresses = getAndConvertAddresses([walletAddress]);
  const merkle = generateMerkle(whitelistAddresses);
  const hashAddress = keccak256(convertedAddresses[0]);
  const proof = merkle?.merkleTree?.getHexProof(hashAddress) || [];
  const verify = merkle?.merkleTree.verify(
    proof,
    hashAddress,
    merkle?.merkleRootHash
  );

  return !!verify;
};

export { generateMerkle, getMerkleProof, isWhiteList, getAndConvertAddresses };

```

In the example above, we created 4 functions.

`getAndConvertAddresses` - check if the address is a correct checksum addresses before we hash the addresses.

`generateMerkle` - a function that generate merkle root hash that we will be using and add in the contract.

`getMerkleProof` - a function that return us proof that will be use for white list mint.

`isWhiteList` -  a function to check if the address is white listed. We can use this function to check in the client-side for e.g


### Generating Merkle Root Hash


Next, we will be using `generateMerkle()` to generate our Merkle root hash and we can either set in our variable in the contract or set it later let's say their changes in the addresses.


![script merkle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ft6lwk94d3sbla7iqg1h.png)

In the example above, we're simple creating a script and console log the output for our merkle root hash. Than we run the script.

`$ npx ts-node scripts/generateMerkle.ts`

The output will look something like this.

![script output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cp6u9yflwbq9aod8je46.png)

For this example, i'am going to just add in our contract. 

![hasehs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nplq2mxq528l3gulel80.png)

___

![coffee break](https://media.giphy.com/media/yNr8LdsnuqdnfbgVXN/giphy.gif)

Phew that was a lot of codes, now let's take a coffee break, no serious you deserve it! ☕️

___

## Mint Phases

There couple of ways to handle mint phases for minting. Like checking the block time stamp if matches a specific timestamp. But for this example for setting public or whitelist phases. We simply add a check for both `mint()` & `whiteListMint()` if the phases return a boolean.  

```javascript
 uint256 public mintPhase;

 function mint(uint256 quantity) external payable {
        // check if public sale is live
        if (mintPhase != 2) revert PublicSaleNotLive();

        ...
    }

    /**
     * @dev a function that only allow whitelisted addresses to mint
     */
    function whiteListMint(uint256 quantity, bytes32[] calldata proof)
        external
        payable
    {
        // check if white list sale is live
        if (mintPhase != 1) revert WhitelistNotLive();

        ...
    }


    /**
     * @dev a function to set mint phase
     */
    function setMintPhase(uint256 _phase) external onlyOwner {
        mintPhase = _phase;
    }

```

In the example above, we create a setMintPhase() which accepts uint256 and only the owner can call this write function. This function allow only the **owner** to set the mint phases as such.

- 1 = whitelist phase.
- 2 = public phase.

___


## TokenURI

As any NFT ERC721 contract, we need to include tokenUri get function to fetch the metadata of of that NFT by token id.

```
 function tokenURI(uint256 _tokenId)
        public
        view
        override
        returns (string memory)
    {
        require(_exists(_tokenId), "token does not exist!");
        if (revealed)
            return
                string(
                    abi.encodePacked(
                        baseURI,
                        _tokenId.toString(),
                        baseExtension
                    )
                );
        else {
            return
                string(
                    abi.encodePacked(
                        baseHiddenUri,
                        _tokenId.toString(),
                        baseExtension
                    )
                );
        }
    }
```

`tokenUri` function accept an uint256 _tokenId, it check if the token exist. In our custom tokenUri function, we added a check for returning a revealed & unrevealed base on the the revealed boolean we added in our contract.

___

## DefaultOperator by Opensea

### How it works?

Token smart contracts may register themselves (or be registered by their "owner") with the OperatorFilterRegistry. Token contracts or their "owner"s may then curate lists of operators (specific account addresses) and codehashes (smart contracts deployed with the same code) that should not be allowed to transfer tokens on behalf of users. 

```
    //  ==========================================
    //  ======== OPERATOR FILTER OVERRIDES =======
    //  ==========================================

    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public payable override onlyAllowedOperator(from) {
        super.transferFrom(from, to, tokenId);
    }

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public payable override onlyAllowedOperator(from) {
        super.safeTransferFrom(from, to, tokenId);
    }

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) public payable override onlyAllowedOperator(from) {
        super.safeTransferFrom(from, to, tokenId, data);
    }
```



# Let's wrap up 🌯

For the rest of the implementation, you can have a look at my open source project here: [nft-smart-contract]("https://github.com/ayepRahman/nft-smart-contract")

It's been a fun project to build while learning through all the required tools/libraries to make this works.

If you like my blog please support me by giving my project ⭐️. You can reach me out at [Linktree](https://linktr.ee/onlyayep). And again thank you for reading 📚 and stay safe you unicorn 🦄!

![thank you](https://media.giphy.com/media/kilGauUai0iA62VIVg/giphy.gif)