Solidity payable NFT minting fails on payment transfer in dual mint contract

I’m working on a Solidity NFT contract that has two different minting methods. One mint requires holding a soulbound token as whitelist proof, and the other is a paid mint where users pay in ETH or MATIC. The soulbound mint works fine, but I’m having issues with the paid mint function.

function purchaseMintToken() public payable whenNotPaused {
    require(
        tokenPrice > 0, "Token price not configured"
    );

    require(
        keccak256(bytes(authorizedUsers[msg.sender].username)) !=
            keccak256(""),
        "User not authorized"
    );

    require(
        keccak256(bytes(authorizedUsers[msg.sender].role)) == keccak256(bytes("purchaser")),
        "Only purchasers allowed to mint"
    );
    uint256 userBalance = address(msg.sender).balance;
    require(
        userBalance >= tokenPrice,
        "insufficient funds for token purchase"
    );

    
    uint256 newTokenId = _currentTokenId.current();
    _currentTokenId.increment();
    tokenOwners[newTokenId] = msg.sender;

    address payable receiver = payable(address(this));
    receiver.transfer(tokenPrice);

    _safeMint(msg.sender, newTokenId);
}

My test setup looks like this:

describe("User with sufficient balance", async function() {
     beforeEach(async function() {
         await this.contract.setTokenPrice(ethers.utils.parseEther("0.1"));
      });
      it("should mint successfully", async function() {
         const transaction = await this.contract.connect(this.testUser).purchaseMintToken();
         const result = await transaction.wait();
         expect(result.status).to.equal(1);
      });
});

The test fails with “Transaction reverted: function call failed to execute” and points to the line where I do receiver.transfer(tokenPrice). The test account has plenty of ETH (999+ from Hardhat). What could be causing this transfer to fail?

Your fundamental issue is that you’re checking the user’s wallet balance but not actually requiring them to send ETH with the transaction. The msg.sender.balance check is pointless here because it doesn’t guarantee they’ve sent any funds to your contract.

Replace require(userBalance >= tokenPrice, "insufficient funds for token purchase"); with require(msg.value >= tokenPrice, "insufficient payment sent"); and completely remove the receiver.transfer(tokenPrice); line. When users call a payable function with ETH, that ETH automatically transfers to your contract - you don’t need to manually transfer it.

Your test is also missing the value parameter. It should be:

const transaction = await this.contract.connect(this.testUser).purchaseMintToken({ value: ethers.utils.parseEther("0.1") });

The current logic is trying to move ETH from the contract to itself, which is why the transfer fails. Use msg.value to handle incoming payments in payable functions.

you’re transfering from the contract to itself which makes no sense lol. the user should send eth WITH the transaction using msg.value, not checking their wallet balance. try require(msg.value >= tokenPrice) instead and remove that weird transfer line - the eth automatically goes to your contract when sent.

hey there! interesting issue you’ve got here… i think i see what might be happening but im curious about a few things first.

so you’re doing receiver.transfer(tokenPrice) where receiver is the contract itself - but wait, shouldn’t the payment flow the other way? like, the user sends ETH TO the contract, not the contract trying to send ETH to itself?

What confuses me is you’re checking msg.sender.balance >= tokenPrice but then trying to transfer FROM the contract TO the contract… that doesn’t make much sense to me. shouldn’t you be using msg.value to check if the user actually sent enough ETH with the transaction?

Also in your test - are you actually sending any ETH when calling the function? i dont see a value parameter in your contract call. the function is payable but if no ETH is being sent, msg.value would be 0.

can you share what your setTokenPrice function looks like? and have you tried logging what tokenPrice is set to when the transaction fails?

one more thing - does your contract have a receive() or fallback function? without those, transfers to the contract might fail even if everything else is correct.

what network are you testing on exactly? sometimes there are weird quirks with different testnets that could cause unexpected behavior.