Building Mock Crypto Ponk NFT Marketplace using ERC721 Smart Contract: A Technical Guide to Contract Development

Building Mock Crypto Ponk NFT Marketplace using ERC721 Smart Contract: A Technical Guide to Contract Development https://celo.academy/uploads/default/optimized/2X/d/de41028240291cf21c37389e231f8f8f101e9bb7_2_1024x576.jpeg
none 0.0 0

Introduction

Welcome to another tutorial by yours truly😉, This tutorial is part of a bigger NFT series and the aim of this is to help one become an expert in building NFT marketplaces as we know, NFTs are the future.
In this tutorial, we would be going through how to integrate your ERC721 smart contract with react.
This is in conjunction with a bigger project and I have explained some other parts of the project in more detail.

In this tutorial, we would walk you through how to write an erc721 smart contract for the mock dripto-ponks marketplace.

Prerequisites

  • Prior knowledge of javascript
  • Familiarity with the command line
  • Basic understanding of blockchain concepts
  • Have some knowledge of solidity and its concepts
  • Have a basic understanding of React. Knowledge of JSX, props, state, and hooks.

Requirements

  • NodeJS from V12.or higher
  • A code editor or text editor. VSCode is recommended
  • A terminal. Git Bash is recommended
  • An Internet Browser and a good internet connection

What is this thing called ERC721?

An ERC721 smart contract on the Celo blockchain is a type of contract that implements the ERC721 standard, which stands for Ethereum Request for Comment 721. The ERC721 standard defines a set of rules and functions that enable the creation and management of unique, non-fungible tokens (NFTs) on the blockchain.

Unlike fungible tokens (such as cryptocurrencies), which are interchangeable and can be exchanged on a one-to-one basis, non-fungible tokens are unique and indivisible. Each ERC721 token represents a distinct item or asset, and it has its own unique identifier or token ID.

The Celo blockchain is a decentralized blockchain platform that aims to provide financial services and applications to users globally. It is compatible with the Ethereum Virtual Machine (EVM), which means that smart contracts written for Ethereum can be deployed and executed on the Celo blockchain.

ERC721 smart contracts on the Celo blockchain allow developers to create and manage unique digital assets, such as digital collectibles, artwork, game items, or real-world assets represented digitally. These contracts define the rules for creating, owning, transferring, and interacting with NFTs.

Smart Contract Development

Remix is a popular web-based integrated development environment (IDE) that I’m sure you’re familiar with if you’ve followed my previous tutorials. It serves as a convenient platform for developers to write, test, and deploy smart contracts specifically designed for the Celo blockchain.

To get started with Remix, you would create a new workspace and proceed to create a new file, which can be named as MyNft.sol for our example.

At the beginning of your MyNft.sol file, it is customary to include a statement that explicitly states the license under which your code is being released, ensuring clarity and compliance.

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

Next, you would import external libraries so you can leverage their pre-existing functionalities, saving development time and ensuring the solidity of our smart contract code.

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";
  1. import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
    This line imports the base ERC721 smart contract from OpenZeppelin. ERC721 defines the standard functions and behavior for non-fungible tokens (NFTs).

  2. import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
    This import statement brings in the ERC721Enumerable extension from OpenZeppelin. ERC721Enumerable builds upon the ERC721 standard and adds extra functionality to enumerate and query the NFTs owned by an address.

  3. import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
    Here, we’re importing the ERC721URIStorage extension from OpenZeppelin. This extension provides additional capabilities to store and retrieve metadata (such as token URIs) associated with each NFT.

  4. import "@openzeppelin/contracts/access/Ownable.sol";
    This line imports the Ownable smart contract from OpenZeppelin. Ownable provides a basic access control mechanism, allowing for ownership-related operations, such as specifying the contract owner and restricting certain functions to be executed only by the owner.

  5. import "@openzeppelin/contracts/utils/Counters.sol";
    The final import statement imports the Counters library from OpenZeppelin. Counters provides a convenient way to manage and track numerical counters, which is often useful for generating unique token IDs or keeping count of various operations.

After this, you would define a smart contract called MyNFT that inherits from several other contracts: ERC721, Ownable, ERC721Enumerable, and ERC721URIStorage.

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

    Counters.Counter private _tokenIdCounter;
    address contractAddress;

The MyNFT contract utilizes the Counters library by using the Counters.Counter data structure, which allows for the management of counters. In this case, it declares a private variable _tokenIdCounter of type Counters.Counter to keep track of the token IDs for the NFTs.

There is a public variable contractAddress of type address, which represents the address of the contract itself.

Following this, you would define a constructor that sets the contractAddress variable to the address of the marketplace contract, and the variable _tokenId is initialized with the value 0.

    constructor(address marketplace) ERC721("Dripto Ponks", "DPT") {
        contractAddress = marketplace;
    }

    uint256 _tokenId = 0;

The constructor function is executed during the contract deployment and takes an argument marketplace representing the address of a marketplace contract. Inside the constructor, the code initializes the MyNFT contract. It first calls the constructor of the inherited ERC721 contract with the arguments “Dripto Ponks” and “DPT”, setting the name and symbol of the NFTs. The contractAddress variable is then assigned the value of the marketplace argument, which represents the address of the marketplace contract.

You would also declare a variable named _tokenId of type uint256 and initializes it with the value 0. This variable will be used to keep track of the current token ID or the next available token ID when creating new NFTs.

Next, you would define a mint function that increments the token ID, mints a new NFT with the updated ID, sets the associated metadata URI, grants approval to the marketplace contract, and finally returns the ID of the minted NFT.

    function mint(string memory uri) external returns (uint256) {
        _tokenId++;
        _mint(msg.sender, _tokenId);
        _setTokenURI(_tokenId, uri);
        setApprovalForAll(contractAddress, true);
        return _tokenId;
    }

This function allows someone to create and mint a new NFT (non-fungible token) by providing a URI (Uniform Resource Identifier) for the token’s metadata. Here’s a breakdown of what the function does:

  • _tokenId++: This line increments the _tokenId variable by one, ensuring a unique token ID for each new NFT that is minted.

  • _mint(msg.sender, _tokenId): The _mint function is called to create and assign ownership of the new NFT. msg.sender represents the address of the caller (the person executing the mint function), and _tokenId is the unique ID of the new NFT.

  • _setTokenURI(_tokenId, uri): This line sets the metadata URI for the newly created NFT. The _setTokenURI function is called, passing the _tokenId and the provided uri as arguments. The URI typically points to a JSON file containing additional information about the NFT.

  • setApprovalForAll(contractAddress, true): This function call grants approval to the contractAddress (the marketplace contract) to manage all the NFTs owned by the caller. This allows the marketplace contract to facilitate transfers or other operations on behalf of the NFT owner.

  • return _tokenId: The function returns the newly generated _tokenId, which represents the ID of the minted NFT. This can be useful for tracking and verification purposes.

Up next, you would define two functions that allows NFT owners to grant permission for their NFTs to be resold and another allows users to retrieve the current token ID

    function resaleApproval(uint256 tokenId) public {
        require(
            ownerOf(tokenId) == msg.sender,
            "You must own this NFT in order to resell it"
        );
        setApprovalForAll(contractAddress, true);
        return;
    }

    function getTokenId() external view returns (uint256) {
        return _tokenId;
    }

The resaleApproval function allows NFT owners to grant permission for their NFTs to be resold. It performs the following steps:

  1. Ownership Verification:
    The function verifies if the caller (msg.sender) is the owner of the NFT with the specified tokenId. If the verification fails, an error message is displayed, indicating that the caller must own the NFT in order to resell it.

  2. Approval for Resale:
    If the ownership verification succeeds, the function grants approval to the marketplace contract (contractAddress) to manage the NFT on behalf of the owner. This approval enables the resale of the NFT through the marketplace.

The getTokenId function allows users to retrieve the current token ID. It follows these steps:

  1. Retrieval:
    The function returns the value of the _tokenId variable, which represents the current token ID. It does not modify the contract’s state and is marked as view, indicating that it only provides read access to the data.

The next functions are override functions that are required by solidity and would need no explanations.

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

    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);
    }
}

Here is the full code for MyNft.sol

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

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 MyNFT is ERC721, Ownable, ERC721Enumerable, ERC721URIStorage {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;
    address contractAddress;

    constructor(address marketplace) ERC721("Dripto Ponks", "DPT") {
        contractAddress = marketplace;
    }

    uint256 _tokenId = 0;

    function mint(string memory uri) external returns (uint256) {
        _tokenId++;
        _mint(msg.sender, _tokenId);
        _setTokenURI(_tokenId, uri);
        setApprovalForAll(contractAddress, true);
        return _tokenId;
    }

    function resaleApproval(uint256 tokenId) public {
        require(
            ownerOf(tokenId) == msg.sender,
            "You must own this NFT in order to resell it"
        );
        setApprovalForAll(contractAddress, true);
        return;
    }

    function getTokenId() external view returns (uint256) {
        return _tokenId;
    }

    // The following functions are overrides required by Solidity.

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

    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);
    }
}

This is the first half of this section, you have written a contract that mints your NFT and also approves the resale of that NFT, In the next section, you would write the marketplace contract where listing and buying happens.

Marketplace Contract

First of all, create a new file called MyNftMarket.sol and just like always, you would include a license identifier specifying the MIT license for the code and an import statement that imports the IERC721 interface from the OpenZeppelin library, which is used for interacting with ERC721 tokens

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

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

From the above, there are two important elements to explain:

  1. SPDX License Identifier:
    The line // SPDX-License-Identifier: MIT is a comment that specifies the license under which the code is being released. In this case, the code is released under the MIT license, which is a widely used open-source license that grants users the freedom to use, modify, and distribute the code while providing limited liability.

  2. Solidity Version and Import Statement:
    The line pragma solidity ^0.8.0; specifies the version of the Solidity programming language that should be used to compile the smart contract. In this case, the contract is intended to be compiled using Solidity version 0.8.0 or any compatible version.

    The next line import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; is an import statement that brings in the IERC721 interface from the OpenZeppelin library. The IERC721 interface defines the standard functions and events for an ERC721 token contract, allowing contracts to interact with ERC721 tokens in a standardized way.

Up next, you would set up a marketplace contract (MyNftMarket) that maintains a collection of listings. Each listing is represented by a Listing data structure and is stored in the _listings mapping using a unique _listingId.

contract MyNftMarket {
	struct Listing {
		address payable seller;
        address payable owner;
		address token;
		uint tokenId;
		uint price;
        bool sold;
	}

	uint private _listingId = 0;
	mapping(uint => Listing) private _listings;
  1. Listing Data Structure:
    The Listing data structure represents a listing of an NFT for sale in the marketplace. It contains the following properties:

    • seller: The address of the seller who listed the NFT for sale.
    • owner: The address of the current owner of the NFT.
    • token: The address of the ERC721 token contract to which the NFT belongs.
    • tokenId: The unique identifier of the NFT.
    • price: The price at which the NFT is listed for sale.
    • sold: A boolean value indicating whether the NFT has been sold or not.
  2. _listingId Variable:
    This variable is of type uint and is used to assign a unique ID to each listing in the marketplace. It starts with a value of 0 and will be incremented for each new listing.

  3. _listings Mapping:
    The _listings mapping is used to store the listings in the marketplace. It maps the _listingId (the unique ID of the listing) to the corresponding Listing data structure.

Following this, you would create a function that allows users to list an ERC721 token for sale in the marketplace.

	function listToken(address token, uint tokenId, uint price) external {
		IERC721(token).transferFrom(msg.sender, address(this), tokenId);

		Listing memory listing = Listing(
			payable(msg.sender),
            payable(address(this)),
			token,
			tokenId,
			price,
            false
		);
		_listings[_listingId] = listing;
		_listingId++;

	}

Here’s a breakdown of what the function does:

  • Transfer of Token Ownership:
    The line IERC721(token).transferFrom(msg.sender, address(this), tokenId); transfers the ownership of the token with the specified tokenId from the caller (msg.sender) to the marketplace contract (address(this)). This ensures that the token is held by the marketplace during the listing process.

  • Listing Creation:
    The code initializes a new Listing structure with the relevant information:

    • seller: The address of the caller, who becomes the seller of the listed token.
    • owner: The marketplace contract’s address, as it temporarily holds ownership during the listing.
    • token: The address of the ERC721 token contract to which the listed token belongs.
    • tokenId: The unique identifier of the listed token.
    • price: The price at which the token is listed for sale.
    • sold: A boolean value indicating whether the token has been sold or not (initialized as false).
  • Listing Storage:
    The newly created Listing structure is stored in the _listings mapping, using the _listingId as the key. The _listingId is then incremented to ensure a unique ID for the next listing.

Up next, you would define two functions, the getListing and getListingLength. The getListing function retrieves the details of a specific listing based on its listingId, while the getListingLength function provides the total count of listings in the marketplace. These functions offer convenient ways to access information about the listings available for sale.

	function getListing(uint listingId) public view returns (Listing memory) {
		return _listings[listingId];
	}

    function getListingLength()public view returns (uint){
        return _listingId;
    }

Here’s a simplified explanation of their functionality:

  • getListing function:
    The getListing function allows users to retrieve the details of a specific listing in the marketplace based on its listingId. Here’s what the function does:

    • Input: The function takes the listingId as an argument, representing the unique identifier of the desired listing.
    • Output: The function returns the complete Listing data structure associated with the provided listingId.
  • getListingLength function:
    The getListingLength function allows users to retrieve the total number of listings currently available in the marketplace. Here’s how it works:

    • Output: The function returns the value of the _listingId variable, which represents the total count of listings created in the marketplace so far.

Next up, you would define the buyToken function facilitates the purchase of a listed token by transferring ownership, handling the payment, and updating the listing details accordingly.

	function buyToken(uint listingId) public payable {
		Listing storage listing = _listings[listingId];

		require(msg.value == listing.price, "Insufficient payment");

		IERC721(listing.token).transferFrom(listing.owner, msg.sender, listing.tokenId);
		payable(listing.seller).transfer(msg.value);

        listing.sold = true;
        listing.price = listing.price * 2;
        listing.owner = payable(msg.sender);
		listing.seller = payable(msg.sender);

	}
  • Listing Retrieval:
    The code retrieves the Listing structure associated with the provided listingId from the _listings mapping. The storage keyword ensures that changes to the listing variable will directly modify the corresponding storage location.

  • Payment Validation:
    The function checks if the value sent with the transaction (msg.value) matches the price of the listing. If the payment is insufficient, the function returns an error message.

  • Token Transfer:
    The function transfers the ownership of the listed token from the listing.owner (marketplace contract) to the msg.sender (the buyer). This is done using the transferFrom function of the ERC721 token contract (listing.token) and specifying the token ID (listing.tokenId).

  • Payment Handling:
    The payment amount (msg.value) is transferred from the buyer to the seller (listing.seller) using the transfer function. This ensures that the seller receives the payment for the purchased token.

  • Listing Update:
    The listing struct is updated to reflect the changes in ownership and status:

    • sold is set to true to indicate that the token has been sold.
    • The price of the listing is doubled, possibly indicating a new listing price for a future sale.
    • The owner and seller are updated to the msg.sender, as the buyer becomes the new owner of the token.

Here is the full code:

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

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

contract MyNftMarket {
	struct Listing {
		address payable seller;
        address payable owner;
		address token;
		uint tokenId;
		uint price;
        bool sold;
	}

	uint private _listingId = 0;
	mapping(uint => Listing) private _listings;

	function listToken(address token, uint tokenId, uint price) external {
		IERC721(token).transferFrom(msg.sender, address(this), tokenId);

		Listing memory listing = Listing(
			payable(msg.sender),
            payable(address(this)),
			token,
			tokenId,
			price,
            false
		);
		_listings[_listingId] = listing;
		_listingId++;

	}

	function getListing(uint listingId) public view returns (Listing memory) {
		return _listings[listingId];
	}

    function getListingLength()public view returns (uint){
        return _listingId;
    }

	function buyToken(uint listingId) public payable {
		Listing storage listing = _listings[listingId];

		require(msg.value == listing.price, "Insufficient payment");

		IERC721(listing.token).transferFrom(listing.owner, msg.sender, listing.tokenId);
		payable(listing.seller).transfer(msg.value);

        listing.sold = true;
        listing.price = listing.price * 2;
        listing.owner = payable(msg.sender);
		listing.seller = payable(msg.sender);

	}
}

Conclusion

Throughout this tutorial, we have explored the creation of an NFT marketplace on the Celo blockchain using smart contracts. By following the provided code snippets and explanations,you must have learnt about ERC721, minting and listing tokens and also the sale of tokens

By combining these elements, readers have gained a practical understanding of how to build an NFT marketplace on the Celo blockchain. This tutorial serves as a solid foundation for further exploration and development of decentralized applications involving NFTs.

Next Steps

This tutorial is to serve as an explanatory guide to writing ERC721 smart contracts. You could see the full project here

About the Author

Daniel Ogbuti is a web3 developer with a passion for teaching as well as learning. I would love to connect on Twitter @daniel_ogbuti and linkedin: Daniel Ogbuti

See you soon

2 Likes

Approved for you to get started. You can manage the tutorial here by changing the category to Proposals > In Progress then Proposals > Review as you complete the tutorial. Thanks!

1 Like

Hi @danielogbuti i will be reviewing this

remove the extra spacing after your tutorial requirement

@thompsonogoyi1t I’ve done it

ok great. at the end of your tutorial, in the tutorial session, please do remove one of the conclusion as you wrote the word conclusion twice… You can leave the the header alone ## Conclusion

@thompsonogoyi1t I’ve done thaf

great job, you can move to publish

One of many of your interesting NFTs series, and trust me, I found them all worthy.