Build a smart contract for an NFT marketplace

Build a smart contract for an NFT marketplace

In this article, we’re going to go step by step on how to build a smart contract for an NFT marketplace. Everything will be explained in simple words

·

13 min read

Table of contents

No heading

No headings in the article.

In this article, we’re going to go step by step on how to build a smart contract for an NFT marketplace. Everything will be explained in simple words so that everyone can follow along.
Let’s get started

Before we start, make sure you have node.js installed on your computer

Let’s get our hands dirty now

First, let’s create a new directory(folder) and name it cyrrus_contract or you can name it however you want. Then to start working in that folder, we’ll need to navigate into that folder using our terminal by typing cd cyrrus_contract. Once that’s done, we’re going to generate our package.json file by typing npm init -y in our terminal(command line). This might take a couple of seconds. Once that process is done, you should be able to see the package.json file inside your folder. Now, back to our command line, we’re going to install a couple of dependencies that will help us during the development process. First, we are going to install all the dependencies that are related to hardhat. You can just copy and paste the following line in your terminal npm install —save-dev hardhat This process might take a couple of seconds. When that's done installing, you’ll have to initialize hardhat in your project by running npx hardhat in your terminal. After clicking ENTER, you should see this below in your terminal

Screenshot 2022-02-13 at 17.53.26.png In our case, we’re going to create a sample project. After choosing that option, click ENTER. The sample project will ask you to install hardhat-waffle and hardhat-ethers, which makes Hardhat compatible with tests built with Waffle. To do so, run npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers in your terminal. Before we go any further, let’s install one more dependency that will make our whole development process faster. We’re going to install the OpenZeppelin dependency that contains ready-to-use contracts and libraries . To do so, run npm i @openzeppelin/contracts in your terminal. At this point, this is how our project structure looks like

Screenshot 2022-02-13 at 18.03.08.png Most of the time, we’ll work inside of the contracts folder

Let’s go inside of the contracts and what you need to do first is to delete Greeter.sol because we’ll create all our files from scratch. Now let’s create a new file called NFT.sol inside the contracts folder. Inside of that file we’ll write down the source of our license and specify the version of solidity that we would like our project to be compiled in.

//SPDX-License-Identifier:UNLICENSED
pragma solidity>0.8.0;

Next, we’ll add the following

//SPDX-License-Identifier:UNLICENSED
pragma solidity>0.8.0;

contract NFT {

}

We’re now going to use the ready-to-use contracts from openZeppelin by importing them as follows:

//SPDX-License-Identifier:UNLICENSED
pragma solidity>0.8.0;

import “@openzeppelin/contracts/token/ERC721/ERC721.sol”;
import “@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol”;
import “@openzeppelin/contracts/utils/Counters.sol”;

contract NFT {

}

After importing those contracts above, we’ll now inherit from them below,

contract NFT is ERC721URIStorage {

}

Now, it’s time to initialize some variables

contract NFT is ERC721URIStorage {
     using Counters for Counters.Counter;
     Counters.Counter private tokenIds;
     address marketAddress;

}

In this code above, we have inherited from Counters.sol which was imported at the top from openzeppelin and we have initiated tokenIds. Basically Counters.sol helps us to increment and decrement safely in solidity. That means that our tokenIds will be incrementing whenever a new token is being minted. More on that later. We are also initiating marketAddress which will hold an address and that address is the address of our market contract(will be created later). Back to development, we’ll now have our constructor function which is a specific function in solidity that runs only on deployment of the contract. In our case, we’re going to receive a parameter in our constructor function and that parameter will be the address of our market contract and we’ll set that to marketAddress.

constructor(address _marketplaceAddress) ERC721(“CYRRUS TOKEN”, “CYT”) {
     marketAddress = _marketplaceAddress;
}

Let’s now create the function that will allow us to mint tokens

function createToken(string memory _tokenURI) public returns (uint) {
     tokenIds.increment();
     uint newId = tokenIds.current();
     _safeMint(msg.sender, newId);
    setTokenURI(newId, _tokenURI);
    setApprovalForAll(marketAddress, true):
    return newId;
}

Let me explain the code above. By default tokenIds which is inherited from Counters.sol starts at 0. This means for the first time createToken will be called, tokenIds will be equal to 0. That’s why we added tokenIds.increment(). Each time this function will be called, the tokenIds will be incremented by 1. If tokenIds was 55, the next time we call createToken, tokenIds will be equal to 56 and so on. After incrementing the tokenIds, we'll get the current value and store that in a variable called newId. Next, we will mint the token by using _safeMint which is a function from ERC721.sol imported from openZeppelin. Basically this code _safeMint(msg.sender, newId) means that we’re assigning the newId to the address(msg.sender) that called this function. After assigning that token to that address we’ll need to add data to that token so that we can be able to know if that token represents a digital art or a video … For that, we use another function imported from ERC721.sol and that function is setTokenURI. This function will assign some metadata to the newId. The metadata in this case is called _tokenURI and will be passed as a parameter to the function . _tokenURI can be the link of the digital art on IPFS or pinata or another storage provider. At this point we have our token minted to msg.sender and that token has some data associated with it. The following step, we’ll allow our market contract to be able to transfer all tokens when we have approved it. Again, we’ll use a function from our imported ERC721.sol contract, and that function is setApprovalForAll. This function will take in the marketAddress to be able to allow the market contract to make all the transfers and the second argument to that function will be a boolean(especially true). Last but not least, we’ll return that newly minted ID so that we can use it elsewhere.

At this point, this is how our file NFT.sol looks like

//SPDX-License-Identifier:UNLICENSED
pragma solidity>0.8.0;

contract NFT is ERC721URIStorage {
     using Counters for Counters.Counter;
     Counters.Counter private tokenIds;
     address marketAddress;

    constructor(address _marketplaceAddress) ERC721(“CYRRUS TOKEN”, “CYT”) {
           marketAddress = _marketplaceAddress;
    }

    function createToken(string memory _tokenURI) public returns (uint) {
         tokenIds.increment();
         uint newId = tokenIds.current();
         _safeMint(msg.sender, newId);
         setTokenURI(newId, _tokenURI);
        setApprovalForAll(marketAddress, true):
        return newId;
    }


}

We’re now done working in that file, let’s create a new file called NFTMarket.sol inside of our contracts folder. As you can guess we’ll start by doing the following:

//SPDX-License-Identifier:UNLICENSED
pragma solidity>0.8.0;

contract NFTMarket {

}

Then we’ll import a couple of contracts and inherit from them

//SPDX-License-Identifier:UNLICENSED
pragma solidity>0.8.0;

import “@openzeppelin/contracts/token/ERC721/ERC721.sol”;
import “@openzeppelin/contracts/token/ERC721/security/ReentrancyGuard.sol”;
import “@openzeppelin/contracts/utils/Counters.sol”;


contract NFT is ReentrancyGuard {

}

Now we have a new concept called ReentrancyGuard, but what is that weird word about? In simple terms, that contract is a helper that prevents a new call in the contract before execution of the previous call is done. This security issue was the subject of a big hack of a DAO in 2016 that led to millions of dollars stolen. Enough of that, let’s continue with our development. Once again here we’re going to initialize some variables that will be used in this contract.

//SPDX-License-Identifier:UNLICENSED
pragma solidity>0.8.0;

import “@openzeppelin/contracts/token/ERC721/ERC721.sol”;
import “@openzeppelin/contracts/token/ERC721/security/ReentrancyGuard.sol”;
import “@openzeppelin/contracts/utils/Counters.sol”;


contract NFT is ReentrancyGuard {
     using Counters for Counters.Counter;
     Counters.Counter private itemIds;
    address payable marketOwner;
    uint listingFee = 0.1 ether;

}

One more time we have initialized Counters the same way we did in NFT.sol and the purpose of it stays the same. This means that itemIds will be incremeted by 1 every time a new item is listed in this marketplace. The second variable we initialized here is marketOwner which will store the address of the person who deploys this contract. We are setting this address to a payable address because we’ll be sending all the listing fees to this address. And lastly we have another variable called listingFee which is, as the name indicates, the fee to list an item on this marketplace. Now, we’re going to set our marketOwner,

constructor() {
     marketOwner = msg.sender;
}

We are using a constructor because we said that the owner of this market should be the address that deployed the contract, this means that the owner should be set when we deploy our contract and guess which function runs only on deployment ? Yeah, you guessed it right, it is the constructor function. To be able to list an NFT on this marketplace, we should define a simple structure of how that NFT item should look like. Let’s do it.

struct MarketItem {
   uint itemId;
   address nftContractAddress;
   uint tokenId;
   address payable owner;
   uint price;
   bool onSale;
}

Let’s now create a mapping that will store all our items(NFTs)

mapping(uint256 => MarketItem) private marketItems;

To be able to access the listingPrice on the frontend, let’s have a simple function that will allow us to read that listingPrice from the frontend.

function getListingPrice() public view returns (uint256) {
   return listingFee;
}

Now that we have our structure of an item and where to store those items, let’s create a function that will help us to mint an NFT in this marketplace.

function mintNFT(address _nftContract, uint256 _tokenId, uint256 _price) public payable nonReentrant {
        require(_price > 0, “Price must be at least 1wei”);
        itemIds.increment();
        uint256 itemId = itemIds.current();
       marketItems[itemId] = MarketItem(itemId, _nftContract, _tokenId, payable(msg.sender), _price, true);
      IERC721(_nftContract).safeTransferFrom(msg.sender, address(this), tokenId);

}

So what we are doing in this function, we are verifying that the price of the NFT is greater than 0 because it doesn’t make sense to set the price of an NFT to 0. Then, we increment the itemIds and store that new value in itemId . Lastly, we’re storing the new item(NFT) in the mapping so that we can easily retrieve that item and send the token to the market contract so that the market can move it to a new owner when it is sold. If we’re able to mint an NFT, that means that we should also be able to sell/buy an NFT. Let’s create a function that will allow us to sell/buy an NFT

function sellNFT (address _nftContract, uint _itemId) public payable nonReentrant {
        uint price = marketItems[_itemId].price;
        uint tokenId = marketItems[_itemId].tokenId;
        require(msg.value == price, “Please, pay the required price”);
       marketItems[_itemId].owner.transfer(msg.value - listingFee);
       IERC721(_nftContract).safeTransferFrom(address(this), msg.sender, tokenId);
       marketItems[_itemId].owner = msg.sender;
       marketItems[_itemId].onSale = false;
      payable(owner).transfer(listingFee);
}

In the function above, we’re first of all retrieving the item with the ID of _itemId from the mapping marketItems. Then we’re accessing some properties like price and tokenId then store them in new values. Then we’re checking that the value that has been sent is equal to the price of that item so that we don’t transfer the item at a lower price than expected. Then the next step is to transfer that value to the current owner of the item. After the transfer is done, we’re now transferring the actual token to the new owner(the address that bought it). After transfering the token, we’re setting owner to msg.sender because that is the address that has bought the item and logically the new owner of the item. At the end, we’re sending the listing fee to the address that owns(deployed) the marketplace.

Now that we have a function to mint and buy NFTs, we should have another function to list all the NFTs that are currently on sale. Let’s work on that next.

 function fetchNFTs() public view returns (MarketItem[] memory) {
       uint256 numberOfItems = itemIds.current();
       MarketItem[] memory items = new MarketItem[](0);
      for(uint i = 1; i <= numberOfItems; i++) {
                if(marketItems[i].onSale == true) {
                       items.push(marketItems[i]);
                }
      }
      return items;
}

In the above function, the goal is to return only items that are on sale. How are we achieving that ? We’re first getting the current number of all items (on sale and not on sale) and storing that in numberOfItems. Then, we’re initiating a new array called items and in which we’ll store items that are on sale. Next step, we’re looping over all items then retrieving all items that are on sale i.e all items that have boolean TRUE and push them to the array of items. Then, we are returning that array. Another alternative of achieving this would be to loop through all the elements then count all the elements that are on sale. Then after getting that number, we would initiate a new array called items with a precise length(which would be the number of items on sale) and continue with another loop to retrieve now those elements as we did it up . But no worries, if that doesn’t make sense, let’s try that second alternative method with the function that we are implementing below. Another function that would make sense to have would be a function to display all items that I own, whether on sale or not. Let’s do that now

function fetchAllMyNFTs() public view returns (MarketItem[] memory) {
       uint256 numberOfItems = itemIds.current();
       uint itemCount = 0; 
       uint currentIndex = 0;
       for(uint i = 1; i <= numberOfItems; i++) {
                if(marketItems[i].owner == msg.sender) {
                       itemCount += 1;
                }
      }

       MarketItem[] memory items = new MarketItem[](itemCount);
      for(uint i = 1; i <= numberOfItems; i++) {
                if(marketItems[i].owner == msg.sender) {
                       items[currentIndex] = marketItems[i]);
                       currentIndex += 1;
                }
      }
      return items;
}

In this function we have done almost the same thing as fetchNFTs but the difference is that we have used a known-size array(i.e the length of the array is known already), and the condition inside of the FOR loop has changed i.e the IF inside of the FOR (just in case someone did not understand).

Let’s add one last function which is crucial to our marketplace, that function will allow a user to re-list their item for sale.

function reSale(address _nftContract, uint _itemId) public payable nonReentrant {
      MarketItem myItem = marketItems[_itemId];
      require(msg.sender == myItem.owner, “You don’t own this item”);
      IERC721(_nftContract).safeTransferFrom(msg.sender, address(this), myItem.tokenId);
      myItem.onSale = true;
}

This function is straightforward. We are retrieving an item with its itemID from the mapping of all items. Then we’re verifying that the address that is trying to list an item for sale is really the owner of that item. If that address is the owner, we’ll proceed on the transfer of that token from the owner to the market so that the market can move that element when it is being sold. Lastly, we are setting the “onSale” boolean to TRUE to clearly show that the item is listed on sale and can be displayed on the marketplace.

This is how our NFTMarket.sol file looks like now

//SPDX-License-Identifier:UNLICENSED
pragma solidity>0.8.0;

import “@openzeppelin/contracts/token/ERC721/ERC721.sol”;
import “@openzeppelin/contracts/token/ERC721/security/ReentrancyGuard.sol”;
import “@openzeppelin/contracts/utils/Counters.sol”;


contract NFT is ReentrancyGuard {
     using Counters for Counters.Counter;
     Counters.Counter private itemIds;
     address payable marketOwner;
     uint listingFee = 0.1 ether;
     constructor() {
       marketOwner = msg.sender;
     }

     struct MarketItem {
            uint itemId;
            address nftContractAddress;
            uint tokenId;
           address payable owner;
           uint price;
           bool onSale;
    }
    mapping(uint256 => MarketItem) private marketItems;

   function getListingPrice() public view returns (uint256) {
        return listingFee;
   }

    function mintNFT(address _nftContract, uint256 _tokenId, uint256 _price) public     payable nonReentrant {
        require(_price > 0, “Price must be at least 1wei”);
        itemIds.increment();
        uint256 itemId = itemIds.current();
       marketItems[itemId] = MarketItem(itemId, _nftContract, _tokenId,        payable(msg.sender), _price, true);
      IERC721(_nftContract).safeTransferFrom(msg.sender, address(this), tokenId);

}

   function sellNFT (address _nftContract, uint _itemId) public payable nonReentrant {
        uint price = marketItems[_itemId].price;
        uint tokenId = marketItems[_itemId].tokenId;
        require(msg.value == price, “Please, pay the required price”);
       marketItems[_itemId].owner.transfer(msg.value - listingFee);
       IERC721(_nftContract).safeTransferFrom(address(this), msg.sender, tokenId);
       marketItems[_itemId].owner = msg.sender;
       marketItems[_itemId].onSale = false;
      payable(owner).transfer(listingFee);
}

  function fetchNFTs() public view returns (MarketItem[] memory) {
       uint256 numberOfItems = itemIds.current();
       MarketItem[] memory items = new MarketItem[](0);
      for(uint i = 1; i <= numberOfItems; i++) {
                if(marketItems[i].onSale == true) {
                       items.push(marketItems[i]);
                }
      }
      return items;
}


function fetchAllMyNFTs() public view returns (MarketItem[] memory) {
       uint256 numberOfItems = itemIds.current();
       uint itemCount = 0; 
       uint currentIndex = 0;
       for(uint i = 1; i <= numberOfItems; i++) {
                if(marketItems[i].owner == msg.sender) {
                       itemCount += 1;
                }
      }

       MarketItem[] memory items = new MarketItem[](itemCount);
      for(uint i = 1; i <= numberOfItems; i++) {
                if(marketItems[i].owner == msg.sender) {
                       items[currentIndex] = marketItems[i]);
                       currentIndex += 1;
                }
      }
      return items;
}

 function reSale(address _nftContract, uint _itemId) public payable nonReentrant {
      MarketItem myItem = marketItems[_itemId];
      require(msg.sender == myItem.owner, “You don’t own this item”);
      IERC721(_nftContract).safeTransferFrom(msg.sender, address(this), myItem.tokenId);
      myItem.onSale = true;
}

}

Congratulations !! You’re among the top 5% who have made it this far. Now you have your own smart contract that you can deploy on any testnet and interact with it. If you want to make one step forward, it is now time to build the frontend of your application so that you can interact with your smart contract from a browser and mint some NFTs.

Nice job !

#WAGMI