Smart contract fails when attempting to accept ERC20 token payments for NFT minting

I’m working on a smart contract for NFT creation and running into issues with ERC20 token payments. The contract works fine when I set it up for free minting or when using native tokens like BNB, but I want users to pay with BUSD tokens instead.

I can successfully check BUSD balances using balanceOf(), but when I try to execute the minting function, I get a gas estimation error that says execution reverted.

Here’s my 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 = 5e17; // 0.5 tokens
    address busdContract = 0xeB3Eb991D39Dac92616da64b7c6D5af5cCFf1627;

    struct TokenData {
        string category;
    }

    mapping(uint256 => TokenData) private _tokenData;

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

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

    function createToken(address owner, 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(owner, 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?

oh interesting, i’m curious about something - why use onlyOwner on the createToken function? seems weird for an nft minting setup.

if users pay BUSD to mint their nfts, shouldn’t they call the function directly? right now only you can call it, but then the approval gets more complicated - users approve the contract, but YOU call the function for them.

planning some backend that calls this after users submit requests? trying to understand your flow here.

quick thing - your creationCost gets set twice, once in declaration (5e17) and again in constructor. clean that up.

yeah approval’s your main issue like others said, but i’m curious about the access control design. what’s your plan for how users actually interact with this?

you gotta approve the contract to spend your BUSD tokens first. the transferFrom won’t work without that approval. call approve() on the BUSD contract with your NFT contract address and the amount before trying to mint.

Same exact issue hit me last month building a marketplace contract. Yeah, the approval’s your main problem, but gas estimation also fails when the EVM simulates and sees the transaction will revert. You need two separate transactions: approve the contract to spend your BUSD with paymentToken.approve(contractAddress, creationCost), then call createToken. Here’s what got me though - you’re approving from the same address that calls mint, right? Since you’ve got onlyOwner on createToken, only the contract owner can call it. If users need to mint, they approve the contract but then YOU have to call createToken for them. That’s a weird flow unless you’re building some managed minting service. Also double-check that BUSD contract address - looks like testnet but make sure it matches your network. I wasted hours debugging because I had the wrong contract address.

Hey! You’ve hit the classic ERC20 approval issue that trips up most people when they start working with token payments.

Lee_Books is right - you’re missing the approval step. But I’m curious - are you calling createToken from another contract or a frontend? I see you’ve got onlyOwner on it, so only the contract owner can call it. Is that what you want?

Quick question about your setup - that BUSD address you hardcoded, is it the testnet one? Just making sure you’re not accidentally pointing to mainnet while testing lol.

One more thing - your transferFrom sends tokens to the contract itself (address(this)). Got plans for a withdraw function? Otherwise those tokens are stuck there forever.

Are you testing the approval + transferFrom flow on Remix or jumping straight to testnet? Sometimes it’s easier to debug token interactions step by step in Remix first.

Everyone already covered the approve() part, but that gas estimation error usually means your transaction will fail before it gets mined. Try calling approve() with more than the creationCost - sometimes decimal rounding screws things up even when your balance looks good.