Maggi, a brand of Nestlé, has recently announced the launch of their first non-fungible token of NFT in the OneRare Foodverse. Now a news like this may not stir much attention as we have been hearing similar launch updates since NFTs started disrupting the system. But it gives a peek into how developers are engaged in creating virtual worlds. And some of them are using ERC721 and ERC1155 for their NFT marketplace. Such practices have triggered the need to make the marketplace more secure and efficient for trading of digital assets.
In this blog, I have explored a way to implement an NFT marketplace smart contract using an NFTTrade contract. My process will give you a peek into this platform’s key features and functionalities and explain how it enables seamless trading of ERC721 and ERC1155 NFTs.
This smart contract platform is built on the Ethereum blockchain. It makes the platform secure and transparent for users. Whether you are an NFT enthusiast or just looking to understand the mechanics of NFT marketplaces, this blog can help you get started.
Overview of NFT Marketplaces Implementation
An NFT marketplace is a platform where you can buy, sell, and trade unique digital assets, which represent the ownership of a specific item. NFT marketplaces use blockchain technology as it offers a decentralized and secure way to keep track of ownership and transactions. Such marketplaces often include tools to manage the supply and demand of NFTs-automated pricing and bidding mechanisms.
To implement an NFT marketplace, you have to work on several aspects. First, you must create NFTs and store them on a blockchain network. It typically involves writing smart contracts that define the unique attributes of each NFT, such as its rarity and provenance.
Once the NFTs are created, build a marketplace platform where users can view and interact with them. You can do it by developing a user-friendly interface and integrating it with various payment systems to handle transactions.
A key component of NFT marketplace implementation is the use of smart contracts. They are self-executing contracts with the terms of a buyer and seller agreement directly written in code. These contracts automate the transaction process and ensure that the terms of the sale are met without intermediaries.
Implementation of NFTTrade Contract
NFTTrade smart contract implements a decentralized exchange for buying and selling ERC721 and ERC1155 tokens. With this contract, users can create a sell order by listing a specific token for sale and setting a unit price. They can also cancel a sell order and create a buy order, specifying the token owner and the number of tokens they want to purchase.
The contract uses the SellOrderSetLib library for managing sell orders. This library allows users to manipulate a data structure called “Set,” which is an array of “SellOrder” objects. The SellOrder objects store information about the address that listed the order, the quantity of tokens available for sale, and the price per token. Whereas, withthe library functions, users can add or remove SellOrders from the set, check if a SellOrder exists in the set, etc.
SellOrderSetLib library
The SellOrderSetLib library provides functions for creating and manipulating a set of SellOrder objects. The SellOrderSetLib is derived from the Hitchens UnorderedKeySet library. Each SellOrder object represents a sell order for a specific token, and contains three properties:
- listedBy: an Ethereum address that listed the sell order.
- quantity: the number of tokens available for sale in this order.
- unitPrice: the price per token in the sell order.
The library provides the following functions for manipulating the set of SellOrders:
insert
: adds a new SellOrder to the set.remove
: removes a SellOrder from the set. The function first checks if the SellOrder exists in the set before removing it.count
: returns the number of SellOrders in the set.exists
: checks if a specific SellOrder exists in the set.orderExistsForAddress
: checks if a sell order exists for a specific seller address.orderAtIndex
: returns the SellOrder at a specific index in the set.orderByAddress
: returns the SellOrder with a specific seller address.nukeSet
: deletes all SellOrders from the set.allOrders
: returns all SellOrders in the set.
This library can be used by other smart contracts to manage a set of sell orders.
Here’s the implementation of the SellOrderSetLib library.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
// Reference to the Hitchens UnorderedKeySet library version 0.93
// https://github.com/rob-Hitchens/UnorderedKeySet
library SellOrderSetLib {
// SellOrder structure representing a sell order, containing the address of the seller, the quantity of tokens being sold, and the unit price of the tokens.
struct SellOrder {
address listedBy; // Address of the seller
uint256 quantity; // Quantity of tokens being sold
uint256 unitPrice; // Unit price of the tokens
}
// Set structure containing a mapping of seller addresses to indices in the keyList array, and an array of SellOrders.
struct Set {
mapping(address => uint256) keyPointers; // Mapping of seller addresses to indices in the keyList array
SellOrder[] keyList; // Array of SellOrders
}
// Function to insert a SellOrder into the Set.
function insert(Set storage self, SellOrder memory key) internal {
// Check if the seller address is address(0), which is not allowed.
require(
key.listedBy != address(0),
"OrderSetLib(100) - Sell Order cannot be listed by address(0)"
);
// Check if the quantity of tokens being sold is greater than 0.
require(
key.quantity > 0,
"OrderSetLib(101) - Sell Order cannot have 0 token count"
);
// Check if the unit price of the tokens is greater than 0.
require(
key.unitPrice > 0,
"OrderSetLib(102) - Sell Order cannot have 0 token price"
);
// Check if the SellOrder is already in the Set.
require(
!exists(self, key),
"OrderSetLib(103) - Key already exists in the set."
);
// If all checks pass, add the SellOrder to the keyList array.
self.keyList.push(key);
// Update the keyPointers mapping with the index of the newly added SellOrder.
self.keyPointers[key.listedBy] = self.keyList.length - 1;
}
function remove(Set storage self, SellOrder memory key) internal {
require(
exists(self, key),
"OrderSetLib(104) - Sell Order does not exist in the set."
);
// Store the last sell order in the keyList in memory
SellOrder memory keyToMove = self.keyList[count(self) - 1];
// Get the row number in keyList that corresponds to the sell order being removed
uint256 rowToReplace = self.keyPointers[key.listedBy];
// Replace the sell order being removed with the last sell order in the keyList
self.keyPointers[keyToMove.listedBy] = rowToReplace;
self.keyList[rowToReplace] = keyToMove;
// Delete the sell order being removed from the keyPointers mapping
delete self.keyPointers[key.listedBy];
// Pop the last sell order from the keyList
self.keyList.pop();
}
/**
* Get the number of sell orders in the set
*
* @param self Set The set of sell orders
* @return uint256 The number of sell orders in the set
*/
function count(Set storage self) internal view returns (uint256) {
return (self.keyList.length);
}
/**
* Check if a sell order already exists in the set
*
* @param self Set The set of sell orders
* @param key SellOrder The sell order to check for existence
* @return bool True if the sell order exists in the set, false otherwise
*/
function exists(Set storage self, SellOrder memory key)
internal
view
returns (bool)
{
if (self.keyList.length == 0) return false;
SellOrder storage o = self.keyList[self.keyPointers[key.listedBy]];
return (o.listedBy == key.listedBy);
}
/**
* Check if a sell order has been listed by a specific address
*
* @param self Set The set of sell orders
* @param listedBy address The address to check for sell orders
* @return bool True if the address has listed a sell order, false otherwise
*/
function orderExistsForAddress(Set storage self, address listedBy)
internal
view
returns (bool)
{
if (self.keyList.length == 0) return false;
SellOrder storage o = self.keyList[self.keyPointers[listedBy]];
return (o.listedBy == listedBy);
}
/**
* Get the sell order at a specific index in the set
*
* @param self Set The set of sell orders
* @param index uint256 The index of the sell order to retrieve
* @return SellOrder The sell order at the specified index
*/
function orderAtIndex(Set storage self, uint256 index)
internal
view
returns (SellOrder storage)
{
return self.keyList[index];
}
/**
* Get the sell order listed by a specific address
*
* @param self Set The set of sell orders
* @param listedBy address The address that listed the sell order to retrieve
* @return SellOrder The sell order listed by the specified address
*/
function orderByAddress(Set storage self, address listedBy)
internal
view
returns (SellOrder storage)
{
return self.keyList[self.keyPointers[listedBy]];
}
/**
* Remove all sell orders from the set
*
* @param self Set The set of sell orders to nuke
*/
function nukeSet(Set storage self) public {
delete self.keyList;
}
/**
* Get all sell orders in the set
*
* @param self Set The set of sell orders
* @return SellOrder[] The array of all sell orders in the set
*/
function allOrders(Set storage self)
internal
view
returns (SellOrder[] storage)
{
return self.keyList;
}
}
NFTTrade Contract
The “NFTTrade” contract allows buying and selling of ERC-721 and ERC-1155 tokens. It provides the following functions:
createSellOrder
: Token owner can use this function to list their token for sale on the platform. Before that the token should be approved for transfer to the NFTTrade contract and the caller should be the owner of the token. The function stores the sell order in a mapping of orders using the “SellOrderSetLib.Set” library.
cancelSellOrder
: Token owner can use this function to cancel their previously created sell order. The token must be listed for sale by the same owner.
createBuyOrder
: Buyers can use this function to place a purchase order for a token. Before that the token should be listed for sale. The buyer must send the correct amount of Ether to the contract along with the purchase order. Once all checks are performed, the token is transferred from seller’s account to the buyer’s account.
getOrders
: This function retrieves the sell orders for the given token.
getOrderByAddress
: It retrieves the SellOrder of a token for a given owner address.
_getOrdersMapId
: This is a private function that generates a unique identifier for each sell order based on the token ID and contract address.
The contract also emits events such as ListedForSale
when a token is listed for sale, UnlistedFromSale
when a sell order is cancelled, and TokensSold
when a token is successfully purchased.
Below is the NFTTrade contract implementation.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
// Import necessary libraries from OpenZeppelin
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/Context.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
// Import SellOrderSetLib
import "./SellOrderSetLib.sol";
// Contract to implement buying and selling of ERC721 and ERC1155 tokens
contract NFTTrade is Context {
// Use SafeMath library for uint256 arithmetic operations
using SafeMath for uint256;
// Use SellOrderSetLib for SellOrderSetLib.Set operations
using SellOrderSetLib for SellOrderSetLib.Set;
// Mapping to store sell orders for different NFTs
mapping(bytes32 => SellOrderSetLib.Set) private orders;
// Event to indicate a token is listed for sale
event ListedForSale(
// Account address of the token owner
address account,
// NFT id
uint256 nftId,
// Contract address of the NFT
address nftContract,
// Number of tokens for sale
uint256 noOfTokensForSale,
// Unit price of each token
uint256 unitPrice
);
// Event to indicate a token is unlisted from sale
event UnlistedFromSale(
// Account address of the token owner
address account,
// NFT id
uint256 nftId,
// Contract address of the NFT
address nftContract
);
// Event to indicate a token is sold
event TokensSold(
// Account address of the token seller
address from,
// Account address of the token buyer
address to,
// NFT id
uint256 nftId,
// Contract address of the NFT
address nftContract,
// Number of tokens sold
uint256 tokenCount,
// Purchase amount
uint256 puchaseAmount
);
/**
* createSellOrder - Creates a sell order for the NFT specified by `nftId` and `contractAddress`.
*
* @param nftId - The ID of the NFT to be sold.
* @param contractAddress - The address of the NFT's contract.
* @param nftType - The type of the NFT, either 'erc721' or 'erc1155'.
* @param unitPrice - The price of a single NFT in wei.
* @param noOfTokensForSale - The number of NFTs being sold.
*/
function createSellOrder(
uint256 nftId,
address contractAddress,
string memory nftType,
uint256 unitPrice,
uint256 noOfTokensForSale
) external {
// Require that the unit price of each token must be greater than 0
require(unitPrice > 0, "NFTTrade: Price must be greater than 0.");
// Get the unique identifier for the sell order
bytes32 orderId = _getOrdersMapId(nftId, contractAddress);
// Get the sell order set for the given NFT
SellOrderSetLib.Set storage nftOrders = orders[orderId];
// Require that the token is not already listed for sale by the same owner
require(
!nftOrders.orderExistsForAddress(_msgSender()),
"NFTTrade: Token is already listed for sale by the given owner"
);
// Check if the NFT token is an ERC721 or ERC1155 token
if (
keccak256(abi.encodePacked(nftType)) ==
keccak256(abi.encodePacked("erc721"))
) {
// Get the ERC721 contract
IERC721 tokenContract = IERC721(contractAddress);
// Require that the caller has approved the NFTTrade contract for token transfer
require(
tokenContract.isApprovedForAll(_msgSender(), address(this)),
"NFTTrade: Caller has not approved NFTTrade contract for token transfer."
);
// Require that the caller owns the NFT token
require(
tokenContract.ownerOf(nftId) == _msgSender(),
"NFTTrade: Caller does not own the token."
);
} else if (
keccak256(abi.encodePacked(nftType)) ==
keccak256(abi.encodePacked("erc1155"))
) {
// Get the ERC1155 contract
IERC1155 tokenContract = IERC1155(contractAddress);
// Require that the caller has approved the NFTTrade contract for token transfer
require(
tokenContract.isApprovedForAll(_msgSender(), address(this)),
"NFTTrade: Caller has not approved NFTTrade contract for token transfer."
);
// Require that the caller has sufficient balance of the NFT token
require(
tokenContract.balanceOf(_msgSender(), nftId) >=
noOfTokensForSale,
"NFTTrade: Insufficient token balance."
);
} else {
// Revert if the NFT token is not of type ERC721 or ERC1155
revert("NFTTrade: Unsupported token type.");
}
// Create a new sell order using the SellOrder constructor
SellOrderSetLib.SellOrder memory o = SellOrderSetLib.SellOrder(
_msgSender(),
noOfTokensForSale,
unitPrice
);
nftOrders.insert(o);
// Emit the 'ListedForSale' event to signal that a new NFT has been listed for sale
emit ListedForSale(
_msgSender(),
nftId,
contractAddress,
noOfTokensForSale,
unitPrice
);
}
/**
* cancelSellOrder - Cancels the sell order created by the caller for a specific NFT token.
*
* @param nftId ID of the NFT token to cancel the sell order for.
* @param contractAddress Address of the NFT contract for the NFT token.
*/
function cancelSellOrder(uint256 nftId, address contractAddress) external {
// Get the unique identifier for the order set of the given NFT token.
bytes32 orderId = _getOrdersMapId(nftId, contractAddress);
// Get the sell order set of the given NFT token.
SellOrderSetLib.Set storage nftOrders = orders[orderId];
// Ensure that the sell order exists for the caller.
require(
nftOrders.orderExistsForAddress(_msgSender()),
"NFTTrade: Given token is not listed for sale by the owner."
);
// Remove the sell order from the set.
nftOrders.remove(nftOrders.orderByAddress(_msgSender()));
// Emit an event indicating that the sell order has been unlisted.
emit UnlistedFromSale(_msgSender(), nftId, contractAddress);
}
/**
* createBuyOrder - Create a buy order for an NFT token.
*
* @param nftId - unique identifier of the NFT token.
* @param contractAddress - address of the NFT contract that holds the token.
* @param nftType - type of the NFT token, either 'erc721' or 'erc1155'.
* @param noOfTokensToBuy - number of tokens the buyer wants to purchase.
* @param tokenOwner - address of the seller who is selling the token.
*/
function createBuyOrder(
uint256 nftId,
address contractAddress,
string memory nftType, // 'erc721' or 'erc1155'
uint256 noOfTokensToBuy,
address payable tokenOwner
) external payable {
// Get the unique identifier for the order set of the given NFT token.
bytes32 orderId = _getOrdersMapId(nftId, contractAddress);
// Get the sell order set of the given NFT token.
SellOrderSetLib.Set storage nftOrders = orders[orderId];
// Check if the token owner has a sell order for the given NFT.
require(
nftOrders.orderExistsForAddress(tokenOwner),
"NFTTrade: Given token is not listed for sale by the owner."
);
// Get the sell order for the given NFT by the token owner.
SellOrderSetLib.SellOrder storage sellOrder = nftOrders.orderByAddress(
tokenOwner
);
// Validate that the required buy quantity is available for sale
require(
sellOrder.quantity >= noOfTokensToBuy,
"NFTTrade: Attempting to buy more than available for sale."
);
// Validate that the buyer provided enough funds to make the purchase.
uint256 buyPrice = sellOrder.unitPrice.mul(noOfTokensToBuy);
require(
msg.value >= buyPrice,
"NFTTrade: Less ETH provided for the purchase."
);
if (
keccak256(abi.encodePacked(nftType)) ==
keccak256(abi.encodePacked("erc721"))
) {
// Get the ERC721 contract
IERC721 tokenContract = IERC721(contractAddress);
// Require that the caller has approved the NFTTrade contract for token transfer
require(
tokenContract.isApprovedForAll(tokenOwner, address(this)),
"NFTTrade: Seller has removeed NFTTrade contracts approval for token transfer."
);
// Transfer ownership of the NFT from the token owner to the buyer.
tokenContract.safeTransferFrom(tokenOwner, _msgSender(), nftId);
} else if (
keccak256(abi.encodePacked(nftType)) ==
keccak256(abi.encodePacked("erc1155"))
) {
// Get the IERC1155 contract
IERC1155 tokenContract = IERC1155(contractAddress);
// Require that the caller has approved the NFTTrade contract for token transfer
require(
tokenContract.isApprovedForAll(tokenOwner, address(this)),
"NFTTrade: Seller has removeed NFTTrade contracts approval for token transfer."
);
// Transfer the specified number of tokens from the token owner to the buyer.
tokenContract.safeTransferFrom(
tokenOwner,
_msgSender(),
nftId,
noOfTokensToBuy,
""
);
} else {
// Revert if the NFT type is unsupported.
revert("NFTTrade: Unsupported token type.");
}
// Send the specified value of Ether from the buyer to the token owner
bool sent = tokenOwner.send(msg.value);
require(sent, "Failed to send Ether to the token owner.");
/**
* Check if the quantity of tokens being sold in the sell order is equal to the number of tokens the buyer wants to purchase.
* If true, it removes the sell order from the list of NFT orders.
* Otherwise, update the sell order by subtracting the number of tokens bought from the total quantity being sold.
*/
if (sellOrder.quantity == noOfTokensToBuy) {
nftOrders.remove(sellOrder);
} else {
sellOrder.quantity -= noOfTokensToBuy;
}
// Emit TokensSold event on successfull purchase
emit TokensSold(
tokenOwner,
_msgSender(),
nftId,
contractAddress,
noOfTokensToBuy,
msg.value
);
}
/**
* getOrders: This function retrieves the sell orders for the given token
* @param nftId unique identifier of the token
* @param contractAddress address of the contract that holds the token
* @return An array of sell orders for the given token
*/
function getOrders(uint256 nftId, address contractAddress)
external
view
returns (SellOrderSetLib.SellOrder[] memory)
{
bytes32 orderId = _getOrdersMapId(nftId, contractAddress);
return orders[orderId].allOrders();
}
/**
* getOrderByAddress: Get the SellOrder of a token for a given owner
* @param nftId unique identifier of the token
* @param contractAddress address of the contract that holds the token
* @param listedBy address of the owner
* @return Sell order of a token for the given owner
*/
function getOrderByAddress(
uint256 nftId,
address contractAddress,
address listedBy
) public view returns (SellOrderSetLib.SellOrder memory) {
// Calculate the unique identifier for the order
bytes32 orderId = _getOrdersMapId(nftId, contractAddress);
// Get the SellOrderSet for the NFT
SellOrderSetLib.Set storage nftOrders = orders[orderId];
// Check if a SellOrder exists for the given owner
if (nftOrders.orderExistsForAddress(listedBy)) {
// Return the SellOrder for the given owner
return nftOrders.orderByAddress(listedBy);
}
// Else, return empty SellOrder
return SellOrderSetLib.SellOrder(address(0), 0, 0);
}
// _getOrdersMapId function generates the unique identifier for a given NFT id and contract address
// The identifier is used as the key to store the corresponding SellOrderSet in the `orders` mapping
// This helps to retrieve and manage the sell orders for a specific NFT efficiently.
function _getOrdersMapId(uint256 nftId, address contractAddress)
internal
pure
returns (bytes32)
{
return keccak256(abi.encodePacked(contractAddress, nftId));
}
}
NFTTrade User Flow
You can sell your token to other interested buyers by listing your ERC-721 or ERC-1155 token for sale on the NFTTrade platform.
You first have to approve the transfer of your ERC-721 or ERC-1155 NFT to the NFTTrade contract.
Next, call the createSellOrder
function and provide the following details:
nftId
: the unique identifier of your token contractAddress
: the address of the ERC-721 or ERC-1155 token contract nftType
: the type of token, either “erc721” or “erc1155”unitPrice
: the price you want to sell your token for noOfTokensForSale
: the number of tokens you want to sell.
If the function executes successfully, an event ListedForSale
will be emitted, indicating that your token has been successfully listed for sale on the NFTTrade platform.
In case, you change your mind and want to cancel your sell order, you can call the cancelSellOrder
function and provide the nftId and contractAddress of your token. If the function executes successfully, an event UnlistedFromSale
will be emitted, indicating that your token is no longer listed for sale.
As a buyer, if you want to purchase a listed token on the NFTTrade platform, call the createBuyOrder
function with the purchase amount along with the following details:
nftId
: the unique identifier of the token you want to buy contractAddress
: the address of the ERC-721 or ERC-1155 token contract nftType
: the type of token, either “erc721” or “erc1155” noOfTokensToBuy
: the number of tokens you want to buy tokenOwner
: the address of the seller.
If the function executes successfully, an event TokensSold
will be emitted, indicating that the token has been successfully sold and transferred to you.
Challenges in Marketplace Implementation
Despite the rapid growth and adoption of NFTs, some challenges require attention for the NFT marketplace to reach its full potential.
Some of the currently unsolved challenges are:
- Scalability: Currently, many NFT marketplaces need help with scalability issues due to the limitations of the blockchain technology. It can lead to slow transaction speeds, high transaction fees, and limited capabilities for handling large volumes of transactions.
- Interoperability: NFT marketplaces operate on different blockchain platforms, making it difficult for NFTs to be easily traded across other marketplaces. It limits the ability of NFTs to reach their full potential as a truly global and decentralized asset class.
- Copyright and Intellectual Property: NFTs have the potential to disrupt traditional ownership and copyright models, but there are still questions and concerns surrounding the protection of intellectual property rights for NFTs. This area requires more clarification and regulation as the NFT market grows.
- Collecting Royalties: Collecting royalties for NFTs is a challenge because the process lacks a standardized way to track ownership, collect payments, and manage royalty agreements. The process often relies on manual methods and can be complicated due to each royalty agreement’s unique and complex nature. The NFT community is exploring new technologies, such as smart contracts and blockchain-based royalty tracking systems to automate the collection of royalties. But these solutions are still in the early stages of development.
Conclusion
With the growing popularity of NFT, be ready to experience changes in the way we all interact with businesses. Certain areas where you can easily replace central databases with NFTs could be land registrars, marriage certificates, etc. Another example, where NFTs can be used would be in retail. Tokenization of high end products will allow consumers to check on the public platform for the authenticity of the item. Latest NFT trends with focus on fun, phygital experiences, usability, education, and more will also benefit from NFTTrade contracts.