How to accept ERC20 token payments for NFT minting instead of native currency?

I’m working on a smart contract for NFT creation and need help with payment integration.

Currently I can mint NFTs for free or charge fees in native tokens like BNB. However, I want to accept BUSD tokens as payment on the BSC network instead.

I can successfully check my BUSD balance using balanceOf, but when I try to mint an NFT, I get this error:

Gas estimation failed with execution reverted error
Returned error: {"jsonrpc":"2.0","error":"execution reverted","id":5075618818565261}

Here’s my current contract code:

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

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

contract ArtToken is ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _itemIds;

    IERC20 public paymentToken;
    uint256 public creationCost = 2e18;
    address stablecoinAddress = 0xeB3Eb991D39Dac92616da64b7c6D5af5cCFf1627;

    struct TokenData {
        string category;
    }

    mapping(uint256 => TokenData) private _tokenData;

    constructor(uint256 cost) ERC721("ArtToken", "ART") {
        paymentToken = IERC20(stablecoinAddress);
        creationCost = cost * 1e18;
    }

    function checkBalance() 
        public view 
        returns (uint256)
    {
        return paymentToken.balanceOf(msg.sender);
    }

    function createToken(address to, string memory uri, string memory category)
        public onlyOwner 
        returns (uint256)
    {   
        require(paymentToken.balanceOf(msg.sender) >= creationCost, "Not enough tokens");
        
        paymentToken.transferFrom(msg.sender, address(this), creationCost);
        
        _itemIds.increment();
        uint256 tokenId = _itemIds.current();
        
        _mint(to, tokenId);
        _tokenData[tokenId].category = category;
        _setTokenURI(tokenId, uri);

        return tokenId;
    }

    function getCategory(uint256 tokenId) 
        public view 
        returns (string memory) 
    {
        return _tokenData[tokenId].category;
    }

    function updateCost(uint256 newCost) 
        external onlyOwner 
    {
        creationCost = newCost * 1e18;
    }
}

What am I missing to make the ERC20 payment work properly?

hmm interesting problem you’ve got there! i think i see what might be hapening here - the error you’re getting is pretty common when dealing with erc20 token approvals.

the main issue is probably that your contract doesn’t have permission to spend the user’s BUSD tokens yet. when you call transferFrom, the ERC20 contract checks if your NFT contract is allowed to spend tokens on behalf of the user, and if there’s no approval, it reverts.

before calling your createToken function, users need to approve your contract to spend their BUSD first. they’d need to call something like:

// In your frontend code
const busdContract = new web3.eth.Contract(ERC20_ABI, busdAddress);
await busdContract.methods.approve(yourNFTContractAddress, creationCost).send({from: userAddress});

also curious - why is your createToken function marked as onlyOwner? if only the owner can mint but users need to pay, how are you planning to handle the user payments? are users supposed to approve the owner’s address instead of the contract?

and just wondering - have you tested the approval process on bsc testnet first? sometimes the gas estimation fails if there are multiple transaction dependencies that aren’t set up properly.

what does your frontend interaction look like? are you handling the approval step before trying to mint?

looks like you’re missing the approval step but also, there’s another issue - your createToken function has onlyOwner modifier, meaning only the contract owner can call it, but you’re trying to deduct tokens from msg.sender. this creates a mismatch because the owner calls the function, but payment comes from someone else. you should either remove the onlyOwner modifier or redesign the payment flow to handle this properly.

The gas estimation failure is happening because your contract logic has a fundamental flaw in the payment flow. You’re using onlyOwner modifier which means only the contract owner can execute the function, but inside the function you’re trying to charge msg.sender (who is the owner) instead of the actual user who should be paying. I ran into similar issues when building my first NFT marketplace. The solution is to restructure your payment logic. Either make the function public so users can call it directly and pay themselves, or if you want to keep owner-only minting, you need to pass the payer’s address as a parameter and handle the approval differently. Also, your contract address in the approval needs to match exactly - double check that users are approving the correct deployed contract address, not the token address. Even a small typo will cause the transferFrom to fail silently during gas estimation.