ERC20: transfer amount exceeds balance at ploygon network

Hi everyone !

I am trying to create 2 smart contracts : one for an ERC20 and the other for sale the ERC20 token. What I want to do now is to buy a token of ERC20 by sending USDT(0xc2132D05D31c914a87C6611C10748AEb04B58e8F) amount to the seller. I am deployed this on ploygon network.

When I call the purchaseTokens function at sales contract , get the error like ERC20: transfer amount exceeds balance.
The sales contract already hold the minted token.

The contract code are below

Thanks for your support !

:1234: Code to reproduce

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

// Token Contract
contract NarutoToken is ERC20, Ownable, ERC20Permit {
    uint256 public constant MAX_SUPPLY = 10_000_000 * 10**18; // 10 million tokens

    constructor(address initialOwner) 
        ERC20("NarutoToken", "NATO") 
        Ownable(initialOwner)
        ERC20Permit("NarutoToken")
    {
        _mint(initialOwner, 1_000_000 * 10**18); // Initial mint of 1 million tokens
    }

    function mint(address to, uint256 amount) public onlyOwner {
        require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
        _mint(to, amount);
    } 
}

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract NarutoTokenSaleContract {
    // Use IERC20 for the custom token to ensure compatibility
    IERC20 public token;
    IERC20 public usdtToken;
    
    address public owner;
    uint256 public tokenPrice; // Price in USDT
    uint256 public totalTokensSold;
    uint256 public maxTokensToSell;

    mapping(address => uint256) public tokensPurchased;

    event TokensPurchased(
        address indexed buyer, 
        uint256 usdtAmount, 
        uint256 tokenAmount
    );
    
    event PriceUpdated(uint256 newPrice);
    event SaleParametersChanged(uint256 maxTokens);

    constructor(
        address _tokenAddress, 
        address _usdtTokenAddress, 
        uint256 _initialPrice,
        uint256 _maxTokensToSell
    ) {
        // Use IERC20 interface for token interaction
        token = IERC20(_tokenAddress);
        usdtToken = IERC20(_usdtTokenAddress);
        owner = msg.sender;
        tokenPrice = _initialPrice; // Price in USDT (e.g., 0.1 * 10**6)
        maxTokensToSell = _maxTokensToSell;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not authorized");
        _;
    }

    function purchaseTokens(uint256 usdtAmount) public {
        // Validate purchase amount
        require(usdtAmount > 0, "Purchase amount must be greater than 0");
        
        // Calculate tokens to receive
        uint256 tokensToReceive = (usdtAmount * 10**18) / tokenPrice;
        
        // Check sale limits
        require(
            totalTokensSold + tokensToReceive <= maxTokensToSell, 
            "Exceeds maximum tokens for sale"
        );

        // Transfer USDT from buyer to contract
        require(
            usdtToken.transferFrom(msg.sender, address(this), usdtAmount), 
            "USDT transfer failed"
        );

        // Transfer tokens to buyer
        require(
            token.transfer(msg.sender, tokensToReceive), 
            "Token transfer failed"
        );

        // Update tracking
        totalTokensSold += tokensToReceive;
        tokensPurchased[msg.sender] += tokensToReceive;

        emit TokensPurchased(msg.sender, usdtAmount, tokensToReceive);
    }

    // Owner functions for contract management
    function updateTokenPrice(uint256 _newPrice) external onlyOwner {
        require(_newPrice > 0, "Invalid price");
        tokenPrice = _newPrice;
        emit PriceUpdated(_newPrice);
    }

    function updateMaxTokensToSell(uint256 _maxTokens) external onlyOwner {
        maxTokensToSell = _maxTokens;
        emit SaleParametersChanged(_maxTokens);
    }

    function withdrawUSDT(uint256 amount) external onlyOwner {
        require(
            usdtToken.transfer(owner, amount), 
            "USDT withdrawal failed"
        );
    }

    function withdrawRemainingTokens() external onlyOwner {
        uint256 remainingTokens = token.balanceOf(address(this));
        require(
            token.transfer(owner, remainingTokens), 
            "Token withdrawal failed"
        );
    }

    // View functions
    function getRemainingTokensForSale() public view returns (uint256) {
        return maxTokensToSell - totalTokensSold;
    }
}

:computer: Environment

polygon mainnet

Hi, welcome to the community! :wave:

So do the caller has enough USDT when the caller account calls the function purchaseTokens()

Yes, the buyer wallet account have enough USDT for purchase and POL token for transaction fee.

Could you please share a failed transaction?

Please check this link

It seems like the account 0x151334d10a69F032F9D1d0f4Be3A9F11335b12e6 does not call the USDT.approve(NarutoTokenSaleContractAddress, Amount)