Search code examples
javascriptblockchainsoliditysmartcontractshardhat

Solidity mint function on multimint smart contract - payable nft not working


I have a smart contract in solidity of an NFT collection. The point is that I have 2 different mints. A mint that needs a whitelist of a soulbound NFT and another mint that would be a sales collection (with a cost in MATIC, ETH whatever). I already have the soulbound mint functional, but I'm not getting the sales mint to work. The safeMintNft() function is triggered to mint this sales collection.

function safeMintNft() public payable whenNotPaused {
    require(
        nftCost > 0, "NFT cost is not set"
    );

    require(
        keccak256(bytes(whitelistedAddresses[msg.sender].discordUser)) !=
            keccak256(""),
        "Address is not whitelisted"
    );

    require(
        keccak256(bytes(whitelistedAddresses[msg.sender].user_type)) == keccak256(bytes("buyer")),
        "Only buyers can mint the NFT"
    );
    uint256 minterBalance = address(msg.sender).balance;
    require(
        minterBalance >= nftCost,
        "not enough tokens to pay for this NFT"
    );

    
    uint256 tokenId = _tokenIdCounter.current();
    _tokenIdCounter.increment();
    tokenIdAddresses[tokenId] = msg.sender;

    address payable contractAddress = payable(address(this));
    contractAddress.transfer(nftCost);

    _safeMint(msg.sender, tokenId);
}

Then I'm testing it with chai. The code snippets that reach this case is below:

describe("User has enough eth to mint", async function() {
     beforeEach(async function() {
         await this.nft.setNftCost(ethers.utils.parseEther("0.1"));
      });
      it("should be able to mint", async function() {
         const tx = await this.nft.connect(this.normalUser).safeMintNft();
         const waitedTx = await tx.wait();
         expect(waitedTx.status).to.equal(1);
      });
});

When it reaches this case, we set the cost to 0.1 ether (and the account normalUser has it because it is a hardhat account with 999+ ether). When it reaches the it statement, I get this error:

Error: Transaction reverted: function call failed to execute
at MelkExp.safeMintNft (contracts/MelkExp.sol:107)

And the line 107 is contractAddress.transfer(nftCost);

Any ideas? If any info is missing, i'll edit or answer in comments


Solution

  • address payable contractAddress = payable(address(this));
    contractAddress.transfer(nftCost);
    

    This snippet tries to transfer nftCost amount of native token (ETH on Ethereum, MATIC on Polygon, ...)

    • from your contract address (caller of the transfer() function)
    • to the same contract address (value of contractAddress assigned from address(this)).

    Which is probably not intended.


    If you're sending native token to a contract (no matter if from a contract or from a user address), the recipient contract needs to implement either the receive() or payable fallback() special functions.

    contract MyContract {
        receive() external payable {
            // the received ETH stays in this contract address
            // so you may want to transfer it to some admin address right away
            // or to implement another function for withdrawal
        }
    }
    

    And if you're aiming to pull nftCost amount of native token from the user wallet, you need to include it with the transaction, and then validate on the contract side.

    That's because the user sends the transaction first - and only after some time (few seconds usually on public networks) the contract function is executed. But the contract is not able to affect value of the transaction retroactively after it's been already signed and broadcasted.

    // execute `safeMintNft()`, sending along 0.1 ETH
    const tx = await this.nft.connect(this.normalUser).safeMintNft({
        value: ethers.utils.parseEther("0.1")
    });
    
    function safeMintNft() public payable whenNotPaused {
        // instead of the `transfer()`
        require(msg.value == nftCost);
    }