0

Problem/Clarification:

I have a NFT minting DApp that mints with multiple ERC20 tokens.

I'm having an issue with the react code which calls the smart contract minting function multiple times.

When a user mints with their preferred ERC20 token, the react code is structured in a way that a user would see 3 meta-mask popups.

  1. The first metamask popup asks the user to approve the NFT smart contract to access their ERC20 token.

  2. The second metamask popup asks the user to approve the transfer of the ERC20 tokens into the smart contract.

  3. The third and final popup allows the user to go ahead and mint by calling the mintWithERCToken(mintAmount, tokenID) function in the smart contract. This part is problematic because once the ERC20 has been transferred, and then the user decides to cancel/reject the mint, the token would have already been transferred into the smart contract.

All three metamask calls requires the spending of gas.

What is the correct sequence of events? What is the correct way to write the react code?

Could someone help restructure the react code?

React Code

async function mintWithCrypto(tokenId) {

Web3EthContract.setProvider(ethereum);
let web3 = new Web3(ethereum);

//get erc20 contract address
var erc20address = await blockchain.smartContract.methods.getCryptotoken(tokenId).call();

//get token contract information
var currency = new web3.eth.Contract(TOKENABI, erc20address);

//get NFT cost
var mintRate = await blockchain.smartContract.methods.getNFTCost(tokenId).call();

//get mint amount and convert to int
var _mintAmount = Number(mintAmount);

//get total cost of NFTs minted
var totalAmount = mintRate * _mintAmount;

let gasLimit = 285000;

//get total gas
let totalGasLimit = String(gasLimit * _mintAmount);

setFeedback(`Minting your NFT, please hold on...`);

//approve contract address for ERC20 token
currency.methods.approve(CONFIG.CONTRACT_ADDRESS, String(totalAmount)).send({from: blockchain.account, gasLimit: String(totalGasLimit)})

//transfer ERC20 token to smart contract. **Problematic code**
.then(await currency.methods.transfer(CONFIG.CONTRACT_ADDRESS, String(totalAmount)).send({from: blockchain.account},
  async function (error, transactionHash) {
    // console.log("Transfer Submitted, Hash: ", transactionHash)
    let transactionReceipt = null
    while (transactionReceipt == null) {
      transactionReceipt = await web3.eth.getTransactionReceipt(transactionHash);
      await sleep(10000)
    }
}))

//mint NFT **Problematic code**
.then(blockchain.smartContract.methods.mintWithERCToken(_mintAmount, tokenId).send({from: blockchain.account, gasLimit: String(totalGasLimit)})
.once("error", (err) => {
  if (err.message == "MetaMask Tx Signature: User denied transaction signature.") {
    setFeedback("Transaction cancelled.");
  } else {
    setFeedback("Sorry, something went wrong please try again later.");
  }
})
.then((receipt) => {
  console.log(receipt);
  setFeedback(`Congratulations! You've minted a ${CONFIG.NFT_NAME}.`);
  dispatch(fetchData(blockchain.account));
})
)
}

Smart Contract code

function mintWithERCToken(uint256 mintAmount, uint256 tokenID) public payable {
    CryptoTokenInfo storage tokens = permittedCrypto[tokenID];
    IERC20 paytoken;
    paytoken = tokens.paytoken;
    uint256 costval;
    costval = tokens.costvalue;
    uint256 supply = totalSupply();

    require(mintAmount > 0, "You need to mint at least 1 NFT");
    require(mintAmount <= maxMintAmount, "Max mint amount per session exceeded");
    require(supply + mintAmount <= maxSupply, "Max NFT exceeded");
        
    if (msg.sender != owner()) {
     //check if the user is whitelisted
     if(onlyWhitelisted == true) {
            require(isWhitelisted(msg.sender), "Sorry, address is not whitelisted");
        } 
        require(msg.value == costval * mintAmount, "Insufficient funds. Please add more funds to address");
    }

        for (uint256 i = 1; i <= mintAmount; i++) {
            require(paytoken.transferFrom(msg.sender, address(this), costval));
            _safeMint(msg.sender, supply + i);
        }
    }

1 Answer 1

1

There are two ways I can think of to make sure a user is whitelisted first before anything else happens. First you need to get the user account which can be accessed with this method:

let account;

web3.eth.getAccounts().then(function(result){
    account = result[0];
});

Method 1: create a public view function on the contract to call from your frontend that takes the user's address and returns a bool if they are whitelisted.

You can also use a mapping(address => bool) public mappingName for this.

Method 2: store an array of whitelisted addresses in your frontend code and use

array.includes("addressString"); this will return true or false.

And second question about approval, IERC721 ( NFTs ) has a "setApprovalForAll" but IERC20 does not. It is safer to approve only the specified amount per transaction.

To answer your comment, the flow should go like this:

  1. approve your contract from the token being transferred
  2. initiate mint function

the contract will transfer the tokens from the sender, you don't have to do that on the front end. Only time you need to send value in a transfer is with ETH. I would also rearrange the contract function a bit.

    function mintWithERCToken(uint256 mintAmount, uint256 tokenID) public payable {
    CryptoTokenInfo storage tokens = permittedCrypto[tokenID];
    IERC20 paytoken;
    paytoken = tokens.paytoken;
    uint256 costval;
    costval = tokens.costvalue;
    uint256 totalCost = costval * mintAmount;
    uint256 supply = totalSupply();
    uint256 senderBal = paytoken.balanceOf(msg.sender);

    require(mintAmount > 0, "You need to mint at least 1 NFT");
    require(
        mintAmount <= maxMintAmount,
        "Max mint amount per session exceeded"
    );
    require(supply + mintAmount <= maxSupply, "Max NFT exceeded");

    if (msg.sender != owner()) {
        //check if the user is whitelisted
        if (onlyWhitelisted == true) {
            require(
                isWhitelisted(msg.sender),
                "Sorry, address is not whitelisted"
            );
        }
        // this will only work with sending eth
        // require(msg.value == costval * mintAmount, "Insufficient funds. Please add more funds to address");
    }
    require(senderBal >= totalCost, "You do not have enough tokens to pay");
    // I would transfer the total cost all at once
    payToken.transferFrom(msg.sender, address(this), totalCost);
    // and only loop through the mint
    for (uint256 i = 1; i <= mintAmount; i++) {
        _safeMint(msg.sender, supply + i);
    }
}
5
  • That is awesome @grizzlywebdev. Thank you for your feedback. I already have smart contract function checks to check if the user is whitelisted. I've implemented front end checks as well. That solves the issue with the whitelist. What do you think about the minting function in the smart contract that accepts ERC20's? Is the logic correct? Nov 14, 2022 at 19:10
  • I'd have to see the full contract to check but it looks solid from the snippet you sent Nov 14, 2022 at 21:22
  • pardon me. I mean, the react code. Is the sequence of events correct? First it would ask the user to approve the NFT contract, then it would transfer the ERC20 tokens from their wallet into the smart contract, and finally, the mintWithERCToken() in the smart contract is called to finally mint the NFT. I feel as though it isn't correct. What do you think? Nov 14, 2022 at 21:43
  • I answered your comment in my answer above, under the bold letters! hope it helps Nov 14, 2022 at 22:12
  • Fantastic. Thank you so much. The smart contract does make sense. Unfortunately, my code does not allow for a 2 step mint process, where the actual ERC20 token gets transferred in the smart contract. The transfer process has to be in the react code. I'll share/update my react code in a few minutes. Nov 15, 2022 at 2:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.