|

Full Stack guide to your first NFT project: Part 3 – Smart Contracts

This is Part 3 of the Full Stack NFT project series. The 4 main categories that we are focusing on in this series are:

  • Generating the NFT images and metadata (Part 1)
  • Uploading both images and metadata to IPFS – This article (Part 2)
  • Writing and deploying ERC721 Smart Contract (This one)
  • Enabling end users to mint NFTs via Metamask on your React website.

The focus in Part 3 is to finally write the Smart Contract for our NFT collection. The Smart Contract will allow our fans/buyers to mint our NFTs and enrich their collection with our beautiful art.

As a general rule of thumb, it’s better to reuse Smart Contract logic as much as possible from well-established and audited libraries.

OpenZeppelin is the most used one. In their smart-contracts repository you’ll find a multitude of NFT smart contracts that can be reused.

Here’s their guide if you want to learn more about the ERC721 token standard.

Writing the Smart Contract

This is not a beginner’s guide to Solidity so I’m assuming you’ve already written a few smart contracts. This contract will be the exact same as my CheekySnails collection that’s live on OpenSea. Let’s dive in.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

import "erc721a/contracts/ERC721A.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract CheekySnails is ERC721A, Ownable {
    uint256 public MAXSUPPLY = 7777;
    uint256 public FREE_LIMIT = 777;
    uint256 public EARLY_LIMIT = 2777;
    uint256 public EARLY_PRICE = 0.01 ether;
    uint256 public MAX_MINTS = 20;
    uint256 public EARLY_MINTS = 10;
    uint256 public PRICE = 0.02 ether;
    string public projName = "Cheeky Snails";
    string public projSym = "SNA";

    bool public DROP_ACTIVE = false;

    string public uriPrefix = "";

    mapping(address => uint256) addressToReservedMints;

    constructor() ERC721A(projName, projSym) {
        setUriPrefix("https://ipfs.io/ipfs/.../");
    }
}

In our imports section, you’ll see that we inherit from ERC721A and Ownable. ERC721A is imported from this npm library: https://www.npmjs.com/package/erc721a and it is an optimized smart contract that saves gas when you want to mint multiple NFTs at once, so it has become sort of the standard, it’s the most used contract to extends for your own NFT projects.

The Ownable allows only the owner to perform certain actions, we’ll see later which ones.

Next we initialize all constants we are going to need, in this case my CheekySnails collection had three steps for minting, initially we had 777 free mint, then from 777 to 2777 we had the price of 0.01 and from 2777 to 7777 the price was 0.02 ETH.

With NFT contracts the only thing left for you to do is to define your minting strategy and pricing. Almost everything else is already built by the imported library. Next, we’re covering the most important mint function.

First, we require that our DROP_ACTIVE variable has been activated, then we being our pricing logic.

    function mint(uint256 numTokens) public payable {
        require(DROP_ACTIVE, "Sale not started");
        if (totalSupply() < FREE_LIMIT) {
            require(
                numTokens > 0 && numTokens <= EARLY_MINTS,
                "mint max 10 tokens"
            );
        } else if (totalSupply() < EARLY_LIMIT) {
            require(
                numTokens > 0 && numTokens <= MAX_MINTS,
                "mint max 20 tokens"
            );
            require(
                msg.value >= EARLY_PRICE * numTokens,
                "not enough ether, price is 0.01 eth"
            );
        } else {
            require(
                numTokens > 0 && numTokens <= MAX_MINTS,
                "mint max 20 tokens"
            );
            require(totalSupply() + numTokens <= MAXSUPPLY, "Sold Out");
            require(
                msg.value >= PRICE * numTokens,
                "not enough ether, price 0.02 eth"
            );
        }
        _safeMint(msg.sender, numTokens);
    }

    function flipDropState() public onlyOwner {
        DROP_ACTIVE = !DROP_ACTIVE;
    }

The totalSupply function returns the minted NFTs so far, so it starts from 0. If our total supply is bellow the free limit of 777 that means we’re still in free mint mode, in this case the user doesn’t pay anything but we just block them from minting more than 10 NFTs at once.

The next step is when the totalSupply is higher than 777 but lower than 2777. In this case, we limit the single mint to 20 tokens and check if they had sent enough ETH for the tokens they wish to mint.

The last step is when the totalSupply is above 2777 but still below our hard limit of 7777. Again we limit the mint to 20 tokens and also make sure that the totalSupply doesn’t go over the limit after they have minted. We also check the price, which is not 0.02.

If all the above conditions pass, we call the _safeMint function from our parent contract and mint the tokens to the user’s address.

The flipDropState function serves for enabling or disabling the mint process. We might want to disable it if some hack happens for example.

function tokenURI(uint256 _tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        require(
            _exists(_tokenId),
            "ERC721Metadata: URI query for nonexistent token"
        );
        return string(abi.encodePacked(_baseURI(), Strings.toString(_tokenId)));
    }

    function setUriPrefix(string memory _uriPrefix) public onlyOwner {
        uriPrefix = _uriPrefix;
    }

    function setPrice(uint256 newPrice) public onlyOwner {
        PRICE = newPrice;
    }

    function setMaxMints(uint256 newMax) public onlyOwner {
        MAX_MINTS = newMax;
    }

    function setSupply(uint256 newSupply) public onlyOwner {
        MAXSUPPLY = newSupply;
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return uriPrefix;
    }

    function withdraw() public onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "Nothing to withdraw");
        Address.sendValue(payable(owner()), balance);
    }

    fallback() external payable {}

    receive() external payable {}

TokenURI function simply checks if the token exists, ie. has been minted, and then just appends the tokenId to our base URL we defined in the constructor.

Then we have a bunch of setters functions and finally our withdraw. The withdraw is the most important one of course, it allows us to withdraw the ETH we have gathered from the sold NFTs. Here we use onlyOwner to secure the function so it’s called only from this contract’s owner.

Last but not least, the fallback and receive functions are blank but they exist just in case someone wants to send us extra money 💸.

That’s all folks, the NFT contract is one of the simplest ones you can write.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *