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";
-
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). -
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. -
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. -
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. -
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 themint
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 provideduri
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 thecontractAddress
(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:
-
Ownership Verification:
The function verifies if the caller (msg.sender
) is the owner of the NFT with the specifiedtokenId
. If the verification fails, an error message is displayed, indicating that the caller must own the NFT in order to resell it. -
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:
- 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 asview
, 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:
-
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. -
Solidity Version and Import Statement:
The linepragma 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 theIERC721
interface from the OpenZeppelin library. TheIERC721
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;
-
Listing
Data Structure:
TheListing
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.
-
_listingId
Variable:
This variable is of typeuint
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. -
_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 correspondingListing
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 lineIERC721(token).transferFrom(msg.sender, address(this), tokenId);
transfers the ownership of the token with the specifiedtokenId
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 newListing
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 asfalse
).
-
Listing Storage:
The newly createdListing
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:
ThegetListing
function allows users to retrieve the details of a specific listing in the marketplace based on itslistingId
. 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 providedlistingId
.
- Input: The function takes the
-
getListingLength
function:
ThegetListingLength
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.
- Output: The function returns the value of the
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 theListing
structure associated with the providedlistingId
from the_listings
mapping. Thestorage
keyword ensures that changes to thelisting
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 thelisting.owner
(marketplace contract) to themsg.sender
(the buyer). This is done using thetransferFrom
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 thetransfer
function. This ensures that the seller receives the payment for the purchased token. -
Listing Update:
Thelisting
struct is updated to reflect the changes in ownership and status:sold
is set totrue
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
andseller
are updated to themsg.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