How to Write an NFT Marketplace Smart Contract in Klaytn
How to Write an NFT Marketplace Smart Contract in Klaytn
Overview
NFT marketplaces are online platforms that allow creators to mint and sell their NFTs to buyers who can purchase them using cryptocurrency. These marketplaces typically provide a user-friendly interface for creating and managing NFTs, including the ability to set prices, royalties, and other terms and conditions. In this article, we will explore how to create an NFT marketplace smart contract on Klaytn.
To establish a robust NFT marketplace on the Klaytn blockchain, three distinct smart contracts are necessary: a Token Smart Contract (ERC20) for managing fungible tokens used within the marketplace ecosystem, an NFT Smart Contract (ERC721) for handling unique and non-fungible assets, and a Marketplace Smart Contract serving as the central infrastructure for marketplace interactions, including buying, selling, bidding, and other essential functionalities. These smart contracts work together to enable secure ownership, seamless transactions, and a fair marketplace environment for participants on the Klaytn blockchain.
Prerequisites
ERC20 Smart Contract
IDE
To make it simple - we’ll use Remix IDE
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract token is ERC20 {
constructor(
string memory name,
string memory symbol,
uint256 initialSupply
) ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
}
}
The contract has a constructor function that takes three parameters: name, symbol, and initialSupply. These parameters represent the desired name, symbol, and initial supply of the token, respectively. Inside the constructor, the ERC20 constructor is invoked with the provided name and symbol to initialize the token with the specified name and symbol.
Finally, within the constructor, the _mint function is called. This function is inherited from ERC20 and is used to mint a specified initialSupply of tokens and assign them to the address that deployed the contract, msg.sender. In other words, the tokens are initially assigned to the creator of the contract.
Compiling the contract
Choose compiler fit with solidity version in smart contract
Deploying the contract
Click on Deploy button
On Environment - choose Injected Provider - Metamask
On Contract - choose MyERC20 - contracts/token.sol
Input name, symbol and decimal for ERC20 token. Example
Name: Brolab Token
Symbol: BRO
Decimals: 8
then click Transact
Confirm transaction
Now we got our ERC20 token: 0x524a6226ECaB7f358e1c66591288c8a7637f7146
ERC721 Smart Contract
We do the same as ERC20 token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract nftcontract is ERC721, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
Counters.Counter private _collectionIds;
mapping (uint256 => string) public _tokenURIs;
mapping (uint256 => string) public _tokenName;
mapping (uint256 => uint256) public _collectionId;
mapping (uint256 => string) public _collectionName;
mapping (uint256 => address) public _collectionOwner;
event MintNFT(uint256 tokenId, address recipient, string tokenURI,string name,uint256 collectionId_);
event CreateCollection(uint256 collectionId,string collectionName,address collectionOwner);
string private _baseURIextended;
constructor(string memory name, string memory symbol) ERC721(name, symbol) {}
function setBaseURI(string memory baseURI_) external onlyOwner {
_baseURIextended = baseURI_;
}
function mintNFT(address recipient, string memory tokenURI,string memory name,uint256 collectionId_)
public
returns (uint256)
{
require (
_collectionOwner[collectionId_] == msg.sender, 'not collection owner'
);
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
// mintWithTokenURI(recipient, newItemId, tokenURI);
_mint(recipient, newItemId);
_setTokenURI(newItemId, tokenURI);
_setName(newItemId,name);
_collectionId[newItemId] = collectionId_;
emit MintNFT(newItemId,recipient,tokenURI,name,collectionId_);
return newItemId;
}
function _setTokenURI(uint256 tokenId, string memory uri) internal {
require(
_exists(tokenId),
'ERC721: URI set of nonexistent token'
);
_tokenURIs[tokenId] = uri;
}
function _setName(uint256 tokenId, string memory name) internal {
require(
_exists(tokenId),
''ERC721Metadata: URI set of nonexistent token'
);
_tokenName[tokenId] = name;
}
function _baseURI() internal view virtual override returns (string memory) {
return _baseURIextended;
}
function createCollection(string memory collectionName_) public {
_collectionIds.increment();
uint256 newCollectionId = _collectionIds.current();
_collectionName[newCollectionId] = collectionName_;
_collectionOwner[newCollectionId] = msg.sender;
emit CreateCollection(newCollectionId,collectionName_,msg.sender);
}
}
The contract imports several files from the OpenZeppelin library, including ERC721.sol for the ERC721 token standard, Ownable.sol for ownership functionality, and Counters.sol for managing counters.
Next, the contract named “nftcontract” is defined, and it inherits from both ERC721 and Ownable. This means that the contract will have all the functionality of ERC721 tokens and also include additional ownership capabilities.
The contract uses the Counters library to create two counters: _tokenIds and _collectionIds. These counters are used to keep track of the token and collection identifiers.
Several mapping variables are declared to store information related to tokens and collections. These mappings include _tokenURIs to store the token URIs, _tokenName to store the token names, _collectionId to store the collection IDs associated with each token, _collectionName to store the collection names, and _collectionOwner to store the addresses of collection owners.
The contract includes events MintNFT and CreateCollection to emit events when a new token is minted and when a new collection is created, respectively.
The constructor function takes in the name and symbol of the NFT and initializes the ERC721 contract with these values.
The contract provides a function setBaseURI to set the base URI for token metadata.
The mintNFT function is used to mint a new NFT. It requires the caller to be the owner of the associated collection. It increments the _tokenIds counter, mints a new token with a unique ID, assigns the token URI, sets the token name, assigns the collection ID, and emits the MintNFT event.
The _setTokenURI and _setName internal functions are used to set the token URI and name, respectively.
The _baseURI function is overridden to return the extended base URI.
The createCollection function allows the anyone to create a new collection. It increments the _collectionIds counter, assigns a unique ID to the collection, sets the collection name, assigns the collection owner, and emits the CreateCollection event.
Now we have our ERC20 contract address and ERC721 contract address - Let’s move to the next part: Building NFT Marketplace Smart Contract.
Marketplace Smart Contract
Step 1: Analyzing the Order Struct
In this step, we will first analyze a struct called “Order” that will be used to represent individual orders within the marketplace. This struct will contain fields for the seller’s address, buyer’s address, token ID, payment token address, and price. By organizing these details within the Order struct, you can effectively manage and track the lifecycle of each order.
Here’s an example code snippet for defining the Order struct:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Order {
address seller; // Address of the seller
address buyer; // Address of the buyer
uint256 tokenId; // ID of the NFT token
address paymentToken; // Address of the payment token
uint256 price; // Price of the order
}
In the above code, we define a struct named “Order” that encapsulates the necessary information for an order in the marketplace. The “seller” field represents the address of the seller, while the “buyer” field represents the address of the buyer. The “tokenId” field stores the ID of the NFT token associated with the order.
The “paymentToken” field is used to store the address of the payment token that will be used for the transaction. This can be an ERC20 token address, enabling flexibility in the payment method within the marketplace. Lastly, the “price” field represents the price of the order.
By grouping these fields together within the Order struct, you can easily access and manage the relevant information for each order in your marketplace. This struct will play a crucial role in the overall functionality of your NFT marketplace smart contract.
Step 2: Declaring Contract Variables
In this step, we will declare the necessary variables for your marketplace contract. These variables will include an instance of the ERC721 contract (nftContract) for managing NFTs, a mapping structure to store orders based on their unique order ID, and additional variables such as feeDecimal, feeRate, and feeRecipient for managing fees within the marketplace.
Here’s an example code snippet for declaring these contract variables:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
contract Marketplace is Ownable {
using Counters for Counters.Counter;
using EnumerableSet for EnumerableSet.AddressSet;
Counters.Counter private _orderIdCount;
struct Order {
address seller; // Address of the seller
address buyer; // Address of the buyer
uint256 tokenId; // ID of the NFT token
address paymentToken; // Address of the payment token
uint256 price; // Price of the order
}
IERC721 public immutable nftContract; // Instance of the ERC721 contract for NFTs
mapping (uint256 => Order) public orders; // Mapping to store orders based on their order ID
uint256 public feeDecimal; // Decimal value for fee calculation
uint256 public feeRate; // Rate of the fee to be charged
address public feeRecipient; // Address to receive the fees
EnumerableSet.AddressSet private _supportedPaymentTokens; // Set of supported payment tokens
// Other contract functions and events...
// Constructor
constructor(
address nftAddress_,
uint256 feeDecimal_,
uint256 feeRate_,
address feeRecipient_
) {
require(nftAddress_ != address(0), "Marketplace: nftAddress_ is zero address");
require(feeRecipient_ != address(0), "Marketplace: feeRecipient is zero address");
nftContract = IERC721(nftAddress_);
_updateFeeRecipient(feeRecipient_);
_updateFeeRate(feeDecimal_, feeRate_);
_orderIdCount.increment();
}
// Other contract functions and events...
}
In the above code, we import the necessary dependencies from the OpenZeppelin library, including IERC721 for the ERC721 token interface, IERC20 for the ERC20 token interface, Ownable for contract ownership, Counters for managing order IDs, and EnumerableSet for managing supported payment tokens.
We then declare a struct named “Order” that we defined in the previous step. This struct represents individual orders in the marketplace and holds the necessary order information.
Next, we declare the contract variables. The “nftContract” variable is declared as an instance of the IERC721 contract, which will be used for managing NFTs within the marketplace.
The “orders” mapping is used to store orders based on their unique order ID. It allows for efficient retrieval and management of orders within the marketplace.
The “feeDecimal,” “feeRate,” and “feeRecipient” variables are used for managing the fees in the marketplace. These variables determine the fee calculation and the address that receives the fees. The “feeDecimal” variable represents the decimal value used for precise fee calculations, while the “feeRate” variable represents the fee rate to be charged. The “feeRecipient” variable stores the address that will receive the fees.
Step 3: Implementing Events
In this step, we will implement events to emit important contract actions. By defining and emitting events, we enable external systems to listen and respond to these actions, facilitating effective communication with your marketplace. Events serve as a way to notify interested parties about specific occurrences within the contract.
Here’s an example code snippet that implements events for the NFT marketplace:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
event OrderAdded(
uint256 indexed orderId,
address indexed seller,
uint256 indexed tokenId,
address paymentToken,
uint256 price
);
event OrderCanceled(
uint256 indexed orderId
);
event OrderMatched(
uint256 indexed orderId,
address indexed seller,
address indexed buyer,
uint256 tokenId,
address paymentToken,
uint256 price
);
event FeeRateUpdated(
uint256 feeDecimal,
uint256 feeRate
);
In the above code, we define four events: OrderAdded, OrderCanceled, OrderMatched, and FeeRateUpdated.
The OrderAdded event is emitted when a new order is added to the marketplace. It includes the orderId, seller, tokenId, paymentToken, and price as indexed parameters, allowing external systems to filter and listen specifically to certain orders.
The OrderCanceled event is emitted when an existing order is canceled. It includes the orderId as an indexed parameter, providing a reference to the canceled order.
The OrderMatched event is emitted when an order is successfully matched, indicating that a buyer has purchased an NFT from a seller. It includes the orderId, seller, buyer, tokenId, paymentToken, and price as indexed parameters, enabling external systems to track and process the order matching.
The FeeRateUpdated event is emitted when the fee rate is updated within the marketplace. It includes the feeDecimal and feeRate as parameters, informing external systems about changes in the fee structure.
By emitting these events at appropriate points in your marketplace smart contract, you provide transparency and enable other systems to interact and respond to important contract actions, enhancing the overall functionality and usability of your NFT marketplace.
Step 4: Writing core functions
In this step, we will implement the core functions of your marketplace contract. These functions are crucial for creating, managing, and executing orders within the marketplace. They enable users to add payment tokens, update fee-related variables, calculate fees, check if an address is the seller of an order, add an order, cancel an order, execute an order, and retrieve order information.
Here’s an example code snippet that includes the implementation of these core functions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
function addPaymentToken(address paymentToken_) external onlyOwner {
require(paymentToken_ != address(0), "Marketplace: paymentToken_ is zero address");
require(_supportedPaymentTokens.add(paymentToken_), "Marketplace: token already supported");
}
function isPaymentTokenSupported(address paymentToken_) public view returns (bool) {
return _supportedPaymentTokens.contains(paymentToken_);
}
modifier onlySupportedPaymentToken(address paymentToken_) {
require(isPaymentTokenSupported(paymentToken_), "Marketplace: unsupported payment token");
_;
}
function addOrder(
uint256 tokenId_,
address paymentToken_,
uint256 price_
) public onlySupportedPaymentToken(paymentToken_) {
require(nftContract.ownerOf(tokenId_) == _msgSender(), "Marketplace: sender is not the owner of the token");
require(
nftContract.getApproved(tokenId_) == address(this) ||
nftContract.isApprovedForAll(_msgSender(), address(this)),
"Marketplace: the contract is unauthorized to manage this token"
);
require(price_ > 0, "Marketplace: price must be greater than 0");
uint256 orderId = _orderIdCount.current();
orders[orderId] = Order(
_msgSender(),
address(0),
tokenId_,
paymentToken_,
price_
);
_orderIdCount.increment();
nftContract.transferFrom(_msgSender(), address(this), tokenId_);
emit OrderAdded(orderId, _msgSender(), tokenId_, paymentToken_, price_);
}
function cancelOrder(uint256 orderId_) external {
Order storage order = orders[orderId_];
require(order.buyer == address(0), "Marketplace: order has a buyer");
require(order.seller == _msgSender(), "Marketplace: sender is not the seller");
uint256 tokenId = order.tokenId;
delete orders[orderId_];
nftContract.transferFrom(address(this), _msgSender(), tokenId);
emit OrderCanceled(orderId_);
}
function executeOrder(uint256 orderId_) external {
Order storage order = orders[orderId_];
require(order.price > 0, "Marketplace: order has been canceled");
require(!isSeller(orderId_, _msgSender()), "Marketplace: buyer must be different from seller");
require(order.buyer == address(0), "Marketplace: order has a buyer");
order.buyer = _msgSender();
uint256 feeAmount = _calculateFee(orderId_);
if (feeAmount > 0) {
IERC20(order.paymentToken).transferFrom(_msgSender(), feeRecipient, feeAmount);
}
IERC20(order.paymentToken).transferFrom(_msgSender(), order.seller, order.price - feeAmount);
nftContract.transferFrom(address(this), _msgSender(), order.tokenId);
emit OrderMatched(orderId_, order.seller, order.buyer, order.tokenId, order.paymentToken, order.price);
}
function getOrderInfo(uint256 orderId_) public view returns (address, uint256, address, uint256) {
Order storage order = orders[orderId_];
return (order.seller, order.tokenId, order.paymentToken, order.price);
}
// Other
Explain Function:
addOrder:
This function allows a seller to add a new order to the marketplace.
The seller specifies the tokenId of the NFT they want to sell, the paymentToken address (the token that buyers will use to pay), and the price of the NFT.
The function checks that the seller is the owner of the NFT, and that the contract is authorized to manage the NFT (either through approval or operator status).
It transfers the ownership of the NFT from the seller to the marketplace contract.
Finally, it emits the OrderAdded event with the relevant details of the newly added order.
executeOrder:
This function allows a buyer to execute an order, indicating their intention to purchase the NFT.
The buyer specifies the orderId of the order they want to execute.
The function checks that the order has not been canceled, the buyer is different from the seller, and the order does not already have a buyer.
It calculates the fee amount based on the order price and the fee rate.
If a fee is applicable, it transfers the fee amount from the buyer to the fee recipient.
It transfers the remaining payment amount from the buyer to the seller.
Finally, it transfers the ownership of the NFT from the marketplace contract to the buyer and emits the OrderMatched event.
cancelOrder:
This function allows a seller to cancel an existing order.
The seller specifies the orderId of the order they want to cancel.
The function checks that the order does not have a buyer and that the caller is the seller of the order.
It transfers the ownership of the NFT back to the seller.
Finally, it emits the OrderCanceled event to indicate the cancellation of the order.
These functions are crucial for the basic functionality of your NFT marketplace contract, enabling users to add orders, execute purchases, and cancel orders as needed.
Full Source code
https://github.com/cuongpo/klaytn_nft_market_place
Conclusion
Congratulations! You have successfully created your own NFT marketplace smart contract on the Klaytn blockchain. By following this tutorial, you have learned how to structure your contract, import dependencies, define the Order struct, declare important variables, implement events, and create essential functions for managing orders within the marketplace. This smart contract can serve as the foundation for a secure, decentralized, and efficient NFT marketplace where users can engage in buying, selling, and trading unique digital assets.