Create a NFT marketplace smart contract


Why do we need a marketplace smart contract ?

ERC721 provides some methods to transfer the ownership of a token. But things get complicated when we don’t just want to transfer it, but to sell it.

  • Which tokens are on sale ?
  • What are the sale prices ?
  • How can I be sure the buyer send me the money ?
  • If I send the money, how can I be sure the seller is gonna send me the token ?

Using a marketplace contract makes it easier for buyers and sellers to find and trade NFTs. In addition, a marketplace contract provides security and trust for both buyers and sellers. Because the contract is stored on the blockchain, it is transparent and can be audited by anyone.

Features of our marketplace

First, we must think of saving the listed tokens and their prices. It can be done with a public mapping. As our ERC721 is enumerable, we can go through all the tokens of the collection and check in the mapping if the token is on sale or not.

The owner should be able to list his token on the marketplace at the price he wants. Of course, the token must not be already listed, and the price must be greater than 0 WEI.

As we want to make some money with our marketplace, we will make the listing method a payable method. The token owner will send some money when calling the listing method, alongs with the token id and sell price, to pay us for our marketplace service.

A token owner must be able to remove his token from the marketplace, if he decide he doesn’t want to sell it anymore.

Anyone should be able to buy a listed token. The buy method must be payable, as the buyer send the ether to buy the token. This method is responsible for

  • the transfer of the token from the seller to the buyer
  • the transfer of the ether from the buyer to the seller
  • the removal of the token from the marketplace

And it does all this in a transactional way. Of course, we need first to make sure that the token is listed in the marketplace and that the buyer sent the right amount of ether to buy it.

How is the marketplace allowed to transfer a token it doesn’t own ?

In the ERC721 standard, the safeTransferFrom function is used to transfer ownership of a non-fungible token from one address to another.

To be able to use it to transfer the token on the behalf of the owner, the smart contract first need to be approved by the owner.

The approve method is part of the ERC721 standard and is used to grant another address permission to transfer a NFT on behalf of the owner.

This is done by setting an “approval” for the NFT, which is stored in a mapping in the contract. The method takes two arguments: the address of the approved account, and the ID of the NFT being approved.

When the approval is set, the approved account is able to use the safeTransferFrom method to transfer the NFT on behalf of the owner.

Marketplace implementation in Solidity

Here is how this marketplace can be implemented

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

import "@openzeppelin/contracts/utils/Strings.sol";
import "./MyToken.sol";

contract Marketplace {
    uint256 listingPrice = 0.0025 ether;

    // Contains the tokens on sale and their prices in ether
    // This mapping is public, so it can be used to check if a token is on sale
    // Given a token id, if the price is > 0, it means the token is on sale on the marketplace
    mapping(uint16 => uint) public priceByTokenId;

    // The token collection
    address tokenContract;
    MyToken myToken;

    // Events that will be emitted when a token is listed or unlisted
    event TokenListed(uint16 tokenId, uint256 price);
    event TokenUnlisted(uint16 tokenId);

    constructor(address _tokenContract) {
        // Connect to the token collection contract
        tokenContract = _tokenContract;
        myToken = MyToken(tokenContract);
    }

    // Modifier to restrict some method to token owner only
    modifier onlyTokenOwner(uint16 tokenId) {
        require(
            msg.sender == myToken.ownerOf(tokenId),
            "Only the owner can list the token on the market"
        );
        _;
    }

    // To know if a token is already listed in the marketplace or not
    function getIsTokenAlreadyListed(
        uint16 tokenId
    ) internal view returns (bool) {
        if (priceByTokenId[tokenId] > 0) {
            return true;
        }
        return false;
    }

    // To list a token for sale in the marketplace
    // Only the owner can call it
    // The owner must send some ether when calling it, to pay the listing price
    function listTokenInMarketplace(
        uint16 tokenId, // the token to sell
        uint256 price // the price for this token
    ) external payable onlyTokenOwner(tokenId) {
        require(price > 0, "Selling price must be at least 1 wei");
        require(
            msg.value >= listingPrice,
            string.concat(
                "Wrong listing price, must be ",
                Strings.toString(listingPrice)
            )
        );
        require(
            getIsTokenAlreadyListed(tokenId) == false,
            "This token is already listed in the marketplace"
        );
        // Check if the marketplace has been approved
        // Else, it'll be impossible to transfer the token to the buyer
        require(
            address(this) == myToken.getApproved(tokenId),
            "The owner must approve the marketplace"
        );

        priceByTokenId[tokenId] = price;

        emit TokenListed(tokenId, price);
    }

    // Internal method to remove a token from the marketplace
    function _unlist(uint16 tokenId) internal {
        if (priceByTokenId[tokenId] > 0) {
            delete priceByTokenId[tokenId];

            emit TokenUnlisted(tokenId);
        }
    }

    // Public method to remove a token from the marketplace
    // Only the owner of the token can call it
    function unlist(uint16 tokenId) external onlyTokenOwner(tokenId) {
        _unlist(tokenId);
    }

    // To buy a token that is listed on the marketplace
    // The buyer must send the some ether, the price of the token
    function buyToken(uint16 tokenId) external payable {
        require(
            priceByTokenId[tokenId] > 0,
            "The token you try to buy is not listed"
        );
        require(
            msg.value >= priceByTokenId[tokenId],
            "The amount sent is lower than the token price"
        );

        // Transfer the token
        address seller = myToken.ownerOf(tokenId);
        myToken.safeTransferFrom(seller, msg.sender, tokenId);

        // Transfer the ether
        payable(seller).transfer(msg.value);

        // Remove the token from the marketplace
        _unlist(tokenId);
    }
}