How to implement custom token payments for NFT marketplace transactions

I’m building an NFT marketplace where users can purchase NFTs using my custom ERC20 token instead of ETH. I have a marketplace contract that uses IERC20 interface to interact with my token called DGX. My main issue is understanding how to properly handle the approval process when someone clicks the buy button on my frontend. I need to figure out how to get the user’s wallet to approve spending their DGX tokens for the purchase.

HERE’S MY MARKETPLACE CONTRACT:

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

import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract NFTMarketplace is ReentrancyGuard, Ownable {
  struct CurrencyInfo{
    IERC20 tokenContract;
    uint256 listingCost;
    uint256 creationCost;
    uint256 itemPrice;
  }

  using Counters for Counters.Counter;
  Counters.Counter private _tokenIds;
  Counters.Counter private _soldItems;
  IERC20 public tokenContract;

  CurrencyInfo[] public AcceptedTokens;

  address payable owner;

  constructor() {
    owner = payable(msg.sender);
  }

  struct MarketItem {
    uint itemId;
    address nftContract;
    uint256 tokenId;
    address payable seller;
    address payable owner;
    uint256 price;
    bool sold;
  }

  mapping(uint256 => MarketItem) private idToMarketItem;

  event MarketItemCreated (
    uint indexed itemId,
    address indexed nftContract,
    uint256 indexed tokenId,
    address seller,
    address owner,
    uint256 price,
    bool sold
  );

  function AddToken (IERC20 _tokenContract,uint256 _listingCost,uint256 _creationCost,uint256 _itemPrice) public onlyOwner {
        AcceptedTokens.push(CurrencyInfo({
            tokenContract:_tokenContract,
            listingCost:_listingCost,
            creationCost: _creationCost,
            itemPrice:_itemPrice
        }));
    }
  
  function approveToken (uint256 _tokenId) public payable nonReentrant{
    CurrencyInfo storage currency = AcceptedTokens[_tokenId];
    tokenContract = currency.tokenContract;
    uint256 maxAmount = 500000000000000000000000000;
    tokenContract.approve(msg.sender, maxAmount);
  }

  function getListingCost(uint256 _tokenId) public view returns (uint256) {
    CurrencyInfo storage currency = AcceptedTokens[_tokenId];
    return currency.listingCost;
  }
  
  function listItem(address nftContract,uint256 tokenId,uint256 _tokenId) public payable nonReentrant {
    CurrencyInfo storage currency = AcceptedTokens[_tokenId];
    tokenContract = currency.tokenContract;
    uint256 listingCost = currency.listingCost;
    uint256 itemPrice = currency.itemPrice;
    require(itemPrice > 0, "Price must be greater than zero");
    require(msg.value == listingCost, "Must pay listing fee");
    _tokenIds.increment();
    uint256 itemId = _tokenIds.current();
    idToMarketItem[itemId] = MarketItem(itemId,nftContract,tokenId,payable(msg.sender),payable(address(0)),itemPrice,false);
    IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
    emit MarketItemCreated(itemId,nftContract,tokenId,msg.sender,address(0),itemPrice,false);
    }

  function purchaseItem(address nftContract,uint256 itemId,uint256 _tokenId) public payable nonReentrant {
    CurrencyInfo storage currency = AcceptedTokens[_tokenId];
    tokenContract = currency.tokenContract;
    uint256 listingCost = currency.listingCost;
    uint price = currency.itemPrice;
    uint tokenId = idToMarketItem[itemId].tokenId;
    tokenContract.approve(msg.sender, price);
    require(msg.value == tokenContract.balanceOf(address(this)), "Insufficient balance");
    idToMarketItem[itemId].seller.transfer(msg.value);
    IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
    idToMarketItem[itemId].owner = payable(msg.sender);
    idToMarketItem[itemId].sold = true;
    _soldItems.increment();
    payable(owner).transfer(listingCost);
  }

  function fetchMarketItems() public view returns (MarketItem[] memory) {
    uint itemCount = _tokenIds.current();
    uint unsoldItemCount = _tokenIds.current() - _soldItems.current();
    uint currentIndex = 0;

    MarketItem[] memory items = new MarketItem[](unsoldItemCount);
    for (uint i = 0; i < itemCount; i++) {
      if (idToMarketItem[i + 1].owner == address(0)) {
        uint currentId = i + 1;
        MarketItem storage currentItem = idToMarketItem[currentId];
        items[currentIndex] = currentItem;
        currentIndex += 1;
      }
    }
    return items;
  }
}

wait, i’m confused about your token approval flow… why are you approving tokens FROM the marketplace TO the user in approveToken? that’s backwards.

users should approve the marketplace to spend their DGX tokens. so the user calls dgxToken.approve(marketplaceAddress, amount) first, then your marketplace uses transferFrom(user, seller, price).

also you’re mixing ETH with ERC20 - purchaseItem does seller.transfer(msg.value) but shouldn’t that transfer ERC20 tokens instead?

have you tested this on testnet? curious how you handle the frontend - using web3.js or ethers for the approval transaction before purchase? would love to hear about your frontend implementation since wallet interactions usually get tricky there

your purchaseItem function’s broken - you’re checking msg.value == tokenContract.balanceOf(address(this)) but msg.value is always 0 for ERC20 transfers. use tokenContract.transferFrom(msg.sender, seller, price) instead. make sure your frontend calls approve() on the token contract first, not the marketplace contract.

I’ve built similar marketplaces and your contract’s got some major issues. You’re mixing ETH payments with ERC20 transfers everywhere, which doesn’t work.

For the frontend, you need two steps. First, users call dgxToken.approve(marketplaceAddress, priceAmount) directly on the DGX contract - not through your marketplace. Then they call purchaseItem.

Your purchaseItem function is broken. Ditch all the msg.value checks - ERC20 transfers don’t send ETH. Use tokenContract.transferFrom(msg.sender, seller, price) to move tokens from buyer to seller. That approval line inside purchaseItem makes no sense either - the marketplace can’t approve tokens it doesn’t own.

Your listItem function charges ETH for listing fees but everything else uses ERC20 tokens. Pick one - hybrid ETH/ERC20 or pure ERC20. This mixing creates logical mess.

Check out OpenSea’s Wyvern protocol for good marketplace patterns.