Build an NFT Marketplace on Linea

Learn how to build and deploy a decentralized NFT marketplace on Linea.

10 min read
Build an NFT Marketplace on Linea
NFTs have become a core part of the web3 ecosystem since the 2021 boom, evolving beyond collectibles into real-world use cases like membership access, asset tokenization, gaming items, and digital identity.
No-code platforms like Phosphor have made it easier than ever for creators and developers to mint, customize, and deploy NFTs through an intuitive Creator Studio and API suite, removing the need to build custom NFT contracts from scratch. However, for developers looking to deepen their understanding of Solidity and web3-native experiences, learning how to build an NFT marketplace remains a foundational skill.
In this guide, we’ll walk through the process of developing and deploying a basic but fully functional NFT marketplace on Linea. By the end, you’ll have the knowledge to launch your own marketplace and gain a stronger grasp of smart contract development, decentralized applications, and web3 interactions.

Prerequisite

  • You should have a basic understanding of Solidity.
  • You should have Nodejs and Foundry installed on your PC.

Overview of the contract

This NFT marketplace will support the following functionalities:
  1. Listing NFTs for sale: Users can set a price and list their NFTs, with details securely stored in the smart contract.
  2. Buying NFTs: Buyers can purchase listed NFTs by paying the required Ether, ensuring a seamless transaction.
  3. Batch buying NFTs: Multiple NFTs can be purchased in a single transaction to reduce gas costs.
  4. Delisting NFTs: Owners can remove their NFTs from sale, returning them to their wallets.

Project set up

We will be using the Foundry framework for this project. With Foundry installed, run the command below to initiate a new project:
forge init market
Open the market folder on Vscode or your favorite code editor, and delete the scripts/counter.s.sol, src/counter.sol, and test/counter.t.sol
Install all dependencies
forge install foundry-rs/forge-std --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commit

Full code of the NFT marketplace smart contract

Create a market.sol file in the`src` folder and add the code below to the file.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "lib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {Test, console} from "forge-std/Test.sol";
contract NFTMarketplace  {
    struct Listing {
        address seller;
        uint256 price;
    }
    // NFT Contract => Token ID => Listing
    mapping(address => mapping(uint256 => Listing)) public listings;
    event Listed(address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
    event Purchase(address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);
    event Delisted(address indexed nftContract, uint256 indexed tokenId, address seller);
    modifier isSeller(address nftContract, uint256 tokenId, address spender) {
        require(listings[address(nftContract)][tokenId].seller == spender, "Not the seller");
        _;
    }
    modifier isListed(address nftContract, uint256 tokenId) {
        require(listings[nftContract][tokenId].price > 0, "Not listed");
        _;
    }
    modifier notListed(address nftContract, uint256 tokenId) {
        require(listings[nftContract][tokenId].price == 0, "Already listed");
        _;
    }
    /**
     * @dev List an NFT for sale.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to list.
     * @param price The price of the NFT in wei.
     */
    function listNFT(address nftContract, uint256 tokenId, uint256 price)
        external
        notListed(nftContract, tokenId)
    {
        require(price > 0, "Price must be greater than zero");
        require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "not owner");
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
        listings[nftContract][tokenId] = Listing(msg.sender, price);
        emit Listed(nftContract, tokenId, msg.sender, price);
    }
    /**
     * @dev Buy a listed NFT.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to buy.
     */
    function buyNFT(address nftContract, uint256 tokenId)
        public
        payable
        isListed(nftContract, tokenId)
    {
        Listing memory listing = listings[nftContract][tokenId];
        require(msg.value >= listing.price, "Insufficient funds");
        // Remove listing
        delete listings[nftContract][tokenId];
        // Transfer payment to the seller
        (bool sent, ) = listing.seller.call{value: listing.price}("");
        require(sent, "Failed to send Ether");
        // Transfer NFT to the buyer
        IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
        emit Purchase(nftContract, tokenId, msg.sender, listing.price);
    }

    function buyMultipleNFT(address nftContract, uint256 [] memory tokenIds)
        external
        payable
    {
        uint256 totalAmount;
        for (uint256 i; i < tokenIds.length; i++){
           Listing memory listing = listings[nftContract][tokenIds[i]];
           totalAmount += listing.price;
        }
        require(msg.value >= totalAmount, "Insufficient funds");
        for (uint256 i; i < tokenIds.length; i++){
           buyNFT(nftContract, tokenIds[i]);   
        }
    }
    /**
     * @dev Delist an NFT from the marketplace.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to delist.
     */
    function delistNFT(address nftContract, uint256 tokenId)
        external
        isListed(nftContract, tokenId)
        isSeller(nftContract, tokenId, msg.sender)
    {
        Listing memory listing = listings[nftContract][tokenId];

        // Remove listing
        delete listings[nftContract][tokenId];
        // Return NFT to the seller
        IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
        emit Delisted(nftContract, tokenId, listing.seller);
    }
}
Copy

Key features of the code

Listing struct
This is essential for implementing features like listing, buying, and delisting in a decentralized NFT marketplace. It provides a clear and efficient way to manage listings while ensuring the marketplace keeps a record of each item’s seller and price—both crucial for its operations.
struct Listing {
        address seller; // NFT seller
        uint256 price;  // NFT price
    }
Copy
Listing mapping
This line of code defines a nested mapping in Solidity, which organizes and stores listings for the NFT marketplace. The outer mapping uses the NFT contract address as the key, allowing the smart contract to categorize listings by their respective NFT collections. This ensures the marketplace can support multiple NFT collections.
The inner mapping uses the token ID (a unique identifier for each NFT) as the key, enabling efficient management of multiple listings under a single NFT contract.
 // NFT Contract => Token ID => Listing
    mapping(address => mapping(uint256 => Listing)) public listings;
Copy
NFT listing
The listNFT function allows users to list their NFTs for sale on the marketplace. It takes three parameters: NFT contract, token ID, and price. The NFT contract refers to the address of the collection from which the user wants to list an NFT, the token ID is the unique identifier of the NFT within that collection, and the price is the amount the user wants to sell the NFT for.
The function first validates that the user is the owner of the NFT and ensures that the price is greater than zero. Once these checks pass, the NFT is transferred from the seller to the marketplace, the listings mapping is updated, and the Listed event is emitted with the correct parameters.
function listNFT(address nftContract, uint256 tokenId, uint256 price)
        external
        notListed(nftContract, tokenId)
    {
        require(price > 0, "Price must be greater than zero");
        require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "not owner");
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
        listings[nftContract][tokenId] = Listing(msg.sender, price);
        emit Listed(nftContract, tokenId, msg.sender, price);
    }
Copy
Buying NFT
The buyNFT function allows users to purchase an NFT listed on the marketplace. It takes two parameters: nftContract, the address of the NFT's smart contract, and tokenId, the unique identifier of the NFT being purchased.
The function first verifies that the buyer has sent enough Ether to cover the NFT's price. Once validated, the listing is removed from the listings mapping, the payment is securely transferred to the seller, and the NFT is transferred from the marketplace contract to the buyer's address, finalizing the purchase.
function buyNFT(address nftContract, uint256 tokenId)
        public
        payable
        isListed(nftContract, tokenId)
    {
        Listing memory listing = listings[nftContract][tokenId];
        require(msg.value >= listing.price, "Insufficient funds");
        // Remove listing
        delete listings[nftContract][tokenId];
        // Transfer payment to the seller
        (bool sent, ) = listing.seller.call{value: listing.price}("");
        require(sent, "Failed to send Ether");
        // Transfer NFT to the buyer
        IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
        emit Purchase(nftContract, tokenId, msg.sender, listing.price);
    }
Copy
Buying multiple NFTs
The buyMultipleNFT function allows a user to purchase multiple NFTs in a single transaction. It first calculates the total price by summing the prices of all NFTs listed in the tokenIds array. The function then verifies that the buyer has sent enough funds to cover the total amount.
Once the funds are validated, the function iterates through the tokenIds array, calling the buyNFT function for each NFT. This ensures that each NFT is purchased individually, the payment is transferred to the seller, and the NFTs are sent to the buyer—following the same process as the buyNFT function.
function buyMultipleNFT(address nftContract, uint256 [] memory tokenIds)
        external
        payable
    {
        uint256 totalAmount;
        for (uint256 i; i < tokenIds.length; i++){
           Listing memory listing = listings[nftContract][tokenIds[i]];
           totalAmount += listing.price;
        }
        require(msg.value >= totalAmount, "Insufficient funds");
        for (uint256 i; i < tokenIds.length; i++){
           buyNFT(nftContract, tokenIds[i]);   
        }
    }
Copy
Delisting NFTs
The delistNFT function allows a seller to remove their NFT from the marketplace. It first verifies that the NFT is listed and that the caller is the rightful owner. Once validated, the listing is deleted from the marketplace, effectively removing the NFT from sale.
The function then transfers the NFT back to the seller, ensuring they regain ownership of their asset. Finally, the Delisted event is emitted to confirm that the NFT has been successfully removed from the marketplace.
function delistNFT(address nftContract, uint256 tokenId)
        external
        isListed(nftContract, tokenId)
        isSeller(nftContract, tokenId, msg.sender)
    {
        Listing memory listing = listings[nftContract][tokenId];

        // Remove listing
        delete listings[nftContract][tokenId];
        // Return NFT to the seller
        IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
        emit Delisted(nftContract, tokenId, listing.seller);
    }
Copy
Key events
The contract emits events for essential marketplace actions:
  • Listed: Triggered when an NFT is listed, recording details such as the seller, price, and NFT information.
  • Purchase: Emitted when a buyer purchases an NFT, ensuring transaction traceability.
  • Delisted: Indicates that an NFT has been successfully removed from the marketplace, maintaining transparency.
event Listed(address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
    event Purchase(address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);
    event Delisted(address indexed nftContract, uint256 indexed tokenId, address seller
Copy

Test suite overview

Create a NFT.sol file in the src folder and add the code below to the file.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";
import "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract NFT is ERC721, Ownable {
    uint256 private _tokenIdCounter;
    string private _baseTokenURI;
    event Minted(address indexed to, uint256 tokenId);
    constructor(string memory name, string memory symbol, string memory baseTokenURI) ERC721(name, symbol) Ownable(msg.sender) {
        _baseTokenURI = baseTokenURI;
    }
    /// @dev Function to mint a new NFT
    function mint(address to) external onlyOwner {
        uint256 tokenId = _tokenIdCounter;
        _tokenIdCounter += 1;
        _safeMint(to, tokenId);
        emit Minted(to, tokenId);
    }
    /// @dev Override base token URI for metadata
    function _baseURI() internal view override returns (string memory) {
        return _baseTokenURI;
    }
    /// @dev Function to update the base token URI
    function setBaseTokenURI(string memory newBaseTokenURI) external onlyOwner {
        _baseTokenURI = newBaseTokenURI;
    }
    /// @dev Function to retrieve the current token ID count
    function getTokenCount() external view returns (uint256) {
        return _tokenIdCounter;
    }
}
Copy
Create a market.t.sol file in the test folder and add the code below to the file.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {NFTMarketplace} from "../src/Market.sol";
import {NFT} from "../src/NFT.sol";
contract MarketTest is Test {
    NFTMarketplace public market;
    NFT public nft;
    address public user1 = makeAddr("user1");
    address public user2 = makeAddr("user2");
    address public user3 = makeAddr("user3");
    function setUp() public {
        nft = new NFT("TestNFT", "tnft", "");
        market = new NFTMarketplace();
        nft.mint(user1);
        nft.mint(user1);
        nft.mint(user1);
    }
    function test_Listing() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        market.listNFT(address(nft), 0, 1 ether);

    }

     function test_BuyingNFT() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        market.listNFT(address(nft), 0, 1 ether);

        vm.startPrank(user2);
        deal(user2, 10 ether);
        market.buyNFT{value: 1 ether}(address(nft), 0);
        assert(nft.ownerOf(0) == user2);

    }

      function test_BuyingMultipleNFT() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        nft.approve(address(market), 1);
        nft.approve(address(market), 2);
        market.listNFT(address(nft), 0, 1 ether);
        market.listNFT(address(nft), 1, 1 ether);
        market.listNFT(address(nft), 2, 1 ether);

        vm.startPrank(user2);
        deal(user2, 10 ether);
        uint256 [] memory nfts = new uint256[] (3);
        nfts[0] = 0;
        nfts[1] = 1;
        nfts[2] = 2; 
        market.buyMultipleNFT{value: 3 ether}(address(nft), nfts);     
    }

     function test_DelistingNFT() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        market.listNFT(address(nft), 0, 1 ether);
        assert(nft.ownerOf(0) == address(market));
        market.delistNFT(address(nft), 0);
        assert(nft.ownerOf(0) == address(user1));

    }
}
Copy
The test suite above ensures that the NFT marketplace contract operates as intended across its core functionalities. Here’s a breakdown of the tests:

Setting up the testing environment

The setUp function initializes the environment by deploying both the NFTMarketplace and NFT contracts. It mints three NFTs to user1 for testing purposes. A mock NFT contract (NFT) is deployed with a mint function to generate NFTs, while the marketplace contract (NFTMarketplace) is instantiated. User1 owns all the minted NFTs, allowing testing of listing and delisting functionalities.

Testing NFT listing

The test_Listing function ensures that user1 can list an NFT for sale on the marketplace.
  • User1 approves the marketplace to transfer tokenId 0.
  • User1 lists the NFT at a price of 1 Ether.
  • The test confirms that the listNFT function works correctly and that the NFT is transferred to the marketplace.

Testing single NFT purchase

The test_BuyingNFT function simulates the purchase of a listed NFT by user2.
  • User1 lists tokenId 0 for 1 Ether.
  • User2 receives 10 Ether and calls the buyNFT function, sending 1 Ether to purchase the NFT.
  • After the purchase, the test verifies that user2 is the new owner of tokenId 0.

Testing multiple NFT purchases

The test_BuyingMultipleNFT function tests the ability to buy multiple NFTs in a single transaction.
  • User1 lists three NFTs (tokenId 0, 1, 2) for 1 Ether each.
  • User2 calls buyMultipleNFT, passing the token IDs and sending 3 Ether.
  • The test ensures that user2 becomes the owner of all three NFTs and that the marketplace correctly processes multiple purchases.

Testing NFT delisting

The test_DelistingNFT function verifies that user1 can delist their NFT from the marketplace.
  • User1 lists tokenId 0 for 1 Ether and calls delistNFT.
  • The NFT is returned to user1, and the test confirms that ownership has reverted back to user1.
Run test
forge test

Deployment and verification on Linea

Add the required variables to the .env file, ensuring it is located at the root level of the project:
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/ // Infura RPC endpoint for connecting to Linea 
PRIVATE_KEY=************************************************* // Wallet private key for signing transactions LINEA_API_KEY=************************* // API key for accessing Linea services via Infura
Copy
Add the details below to foundry.toml
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Copy
Create a Makefile and add the details below:
deploy:
    forge create src/Market.sol:NFTMarketplace --rpc-url $(LINEA_RPC_URL) --private-key $(PRIVATE_KEY)

verify:; forge verify-contract --rpc-url $(LINEA_RPC_URL) --chain linea <contract address> src/Market.sol:NFTMarketplace
Copy
Deployment
make deploy
Verification
make verify

Conclusion

Building an NFT marketplace on Linea provides developers with hands-on experience in smart contract development, Solidity programming, and web3 interactions. In this guide, we covered the core functionalities of a decentralized marketplace, including listing, buying, batch purchasing, and delisting NFTs, while ensuring secure and efficient transactions.
We also walked through the implementation and testing of the smart contract using Foundry, as well as the deployment and verification process on Linea. By following this guide, you now have the foundational knowledge to build, test, and deploy your own NFT marketplace.
For more hands-on tutorials and deep dives into blockchain development, check out the MetaMask Developer YouTube channel, and stay updated with our latest insights by following us on Twitter.

This article is written by:

  • Kingsley Okonkwo
    Kingsley Okonkwo

    Kingsley, a certified Ethereum Blockchain developer, serves as a technical writer at Consensys, with a special focus on developer tools. Before joining Consensys, Kingsley worked as a freelance backend developer on sevral platforms including Gigster and Braintrust. Boasting 5 years of software development experience, he's proficient with tools such as JavaScript (Node.js & React), GoLang, SQL, and Docker. Kingsley excels in producing step-by-step tutorials and how-to guides, utilizing the latest in web3 technology to help fellow developers build better dapps. Currently based in Dubai, Kingsley enjoys playing basketball and soccer in his free time

    Read all articles