Setting up ERC20 token payments for NFT marketplace transactions

Need help with custom token integration

I’m building an NFT marketplace where users can purchase digital collectibles using my custom ERC20 token called DGC. I have the basic structure set up but I’m struggling with the approval mechanism.

The main issue: When someone clicks the purchase button on my frontend, how do I properly handle the token approval from their wallet to complete the transaction?

I’m using the IERC20 interface to interact with my token contract, but I’m not sure if my current implementation is correct. The marketplace should allow users to buy and sell NFTs exclusively with DGC tokens.

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 PaymentToken{
    IERC20 tokenContract;
    uint256 listingCost;
    uint256 creationCost;
    uint256 itemPrice;
  }

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

  PaymentToken[] 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 addPaymentToken(IERC20 _tokenContract, uint256 _listingCost, uint256 _creationCost, uint256 _itemPrice) public onlyOwner {
    AcceptedTokens.push(PaymentToken({
      tokenContract: _tokenContract,
      listingCost: _listingCost,
      creationCost: _creationCost,
      itemPrice: _itemPrice
    }));
  }

  function executePayment(uint256 _tokenIndex) public payable nonReentrant {
    PaymentToken storage token = AcceptedTokens[_tokenIndex];
    tokenContract = token.tokenContract;
    uint256 maxAmount = 500000000000000000000000000;
    tokenContract.approve(msg.sender, maxAmount);
  }

  function checkAllowance(uint256 _tokenIndex) public payable nonReentrant {
    PaymentToken storage token = AcceptedTokens[_tokenIndex];
    tokenContract = token.tokenContract;
    tokenContract.allowance(address(this), msg.sender);
  }

  function getListingCost(uint256 _tokenIndex) public view returns (uint256) {
    PaymentToken storage token = AcceptedTokens[_tokenIndex];
    return token.listingCost;
  }

  function listItem(address nftContract, uint256 tokenId, uint256 _tokenIndex) public payable nonReentrant {
    PaymentToken storage token = AcceptedTokens[_tokenIndex];
    tokenContract = token.tokenContract;
    uint256 listingCost = token.listingCost;
    uint256 itemPrice = token.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 completePurchase(address nftContract, uint256 itemId, uint256 _tokenIndex) public payable nonReentrant {
    PaymentToken storage token = AcceptedTokens[_tokenIndex];
    tokenContract = token.tokenContract;
    uint256 listingCost = token.listingCost;
    uint256 itemPrice = token.itemPrice;
    uint256 tokenId = idToMarketItem[itemId].tokenId;
    
    tokenContract.approve(msg.sender, itemPrice);
    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);
  }
}

Any guidance on the proper workflow would be really helpful!

Your approval flow’s totally broken. You can’t use msg.value with ERC20 tokens - that’s only for ETH. The user needs to approve YOUR contract to spend their DGC tokens, not the reverse. Ditch that executePayment function and handle approvals on the frontend by calling dgc.approve(marketplaceAddress, amount) before the purchase.

Hey! I’ve been working on something similar and spotted a few things that might be causing issues.

First - are you handling the approval flow right on your frontend? Your executePayment function has the marketplace approving tokens TO the user, which seems backwards. Users need to approve the marketplace contract to spend their tokens first.

What’s your frontend workflow? Are you prompting users to call approve() on the DGC token contract before they hit purchase?

Also curious - in your completePurchase function, you’re checking msg.value == tokenContract.balanceOf(address(this)) but then using msg.value for the transfer. Shouldn’t you use transferFrom to move the ERC20 tokens instead of dealing with msg.value (which is ETH)?

I’m wondering if you’re mixing up the ETH payment flow with the ERC20 flow? Listing still requires ETH for the listing fee but purchases should be pure DGC tokens, right?

What errors are you seeing when users try to make purchases? Are transactions failing or just not getting triggered?

Your contract has some major issues with the token approval workflow. The biggest problem is in completePurchase - you’re mixing ETH and ERC20 logic when they don’t work together.

Drop msg.value completely for ERC20 tokens. That’s only for ETH. You need transferFrom to pull tokens from the buyer’s wallet to your marketplace, then transfer the seller’s cut.

Here’s how it should work: buyer calls approve() on your DGC token contract first (handle this on the frontend), then calls completePurchase(). Use tokenContract.transferFrom(msg.sender, address(this), itemPrice) to collect payment, then tokenContract.transfer(seller, sellerAmount) to pay the seller.

Your executePayment function is backwards. The marketplace doesn’t approve tokens to users - users approve tokens to the marketplace. Delete that function and handle approvals on the frontend by calling DGC token’s approve() method directly.

Also, msg.value == tokenContract.balanceOf(address(this)) makes zero sense since you’re comparing ETH value to token balance. Just check if the user has enough token balance and allowance before executing the transfer.