2

I'm trying to build fraction NFT Marketplace, It's now working when I try to create a new NFT and show it in the frontend. I'm now facing an error in my buyNFT function and no matter what I try it keeps happening.

Error: VM Exception while processing transaction: reverted with reason string 'ERC20: insufficient allowance'

can anyone guide me on what I should do?

this is my smart contract right now:

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

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "./CustomERC20.sol";

contract FractionalNFTMarketplace is ERC721URIStorage, Ownable {
    using Strings for uint256;

    uint256 private _currentTokenId = 0;
    address public customERC20Address;

    struct FractionalToken {
        CustomERC20 token;
        uint256 totalShares;
        uint256 pricePerShare;
    }

    struct MarketItem {
        uint256 tokenId;
        address payable seller;
        address payable owner;
        uint256 totalShares;
        uint256 pricePerShare;
        bool isForSale;
    }

    mapping(uint256 => FractionalToken) private _fractionalTokens;
    mapping(uint256 => mapping(address => uint256)) private _shareOwners;
    mapping(uint256 => MarketItem) private _marketItems;

    Counters.Counter private _marketItemId;

    event TokenCreated(uint256 indexed tokenId, uint256 totalShares, uint256 pricePerShare);
    event TokenBought(uint256 indexed tokenId, address indexed buyer, uint256 shares);
    event MarketItemCreated(uint256 indexed tokenId, address indexed seller, uint256 totalShares, uint256 pricePerShare);
    event MarketItemSold(uint256 indexed marketItemId, address indexed buyer);
    event ApprovalForToken(uint256 indexed tokenId, address indexed owner, uint256 amount);

    constructor(address _customERC20Address) ERC721("Fractional NFT Marketplace", "FNFTM") {
        customERC20Address = _customERC20Address;
    }
    

    function createMarketItem(uint256 tokenId) private {
    require(_exists(tokenId), "Token ID does not exist.");

    address currentOwner = ownerOf(tokenId);

    FractionalToken storage fractionalToken = _fractionalTokens[tokenId];

    _marketItems[tokenId] = MarketItem(
        tokenId,
        payable(currentOwner),
        payable(address(this)),
        fractionalToken.totalShares,
        fractionalToken.pricePerShare,
        true
    );

    emit MarketItemCreated(tokenId, currentOwner, fractionalToken.totalShares, fractionalToken.pricePerShare);
}

    function createToken(uint256 totalShares, uint256 pricePerShare, string memory tokenURI) public payable returns (uint256) {
        require(totalShares > 0, "Total shares must be greater than 0.");
        require(pricePerShare > 0, "Price per share must be greater than 0.");
        
        uint256 newTokenId = _currentTokenId++;
        _mint(msg.sender, newTokenId);
        _setTokenURI(newTokenId, tokenURI);

        string memory name = string(abi.encodePacked("Shares of NFT #", newTokenId.toString()));
        string memory symbol = string(abi.encodePacked("SNFT", newTokenId.toString()));
        CustomERC20 newERC20Token = new CustomERC20(name, symbol);
        newERC20Token.mint(msg.sender, totalShares);

            _fractionalTokens[newTokenId] = FractionalToken(newERC20Token, totalShares, pricePerShare);
            _shareOwners[newTokenId][msg.sender] = totalShares;
            approveContractForToken(newTokenId,totalShares,address(this));
            createMarketItem(newTokenId);
            newERC20Token.approve(address(this), totalShares);
    
            emit TokenCreated(newTokenId, totalShares, pricePerShare);
            return newTokenId;
        }
function approveContractForToken(uint256 tokenId, uint256 amount, address spender) public {
        require(_exists(tokenId), "Token ID does not exist.");
        require(ownerOf(tokenId) == msg.sender, "Caller is not the owner of the token.");

        FractionalToken storage fractionalToken = _fractionalTokens[tokenId];
        fractionalToken.token.approve(spender, amount);
        emit ApprovalForToken(tokenId, msg.sender, amount);
    }

    function getAllowance(uint256 tokenId, address owner, address spender) public view returns (uint256) {
        require(_exists(tokenId), "Token ID does not exist.");
        FractionalToken storage fractionalToken = _fractionalTokens[tokenId];
        return fractionalToken.token.allowance(owner, spender);
    }

    function buyToken(uint256 tokenId,uint256 totalPrice, uint256 shares) public payable {
        require(_exists(tokenId), "Token ID does not exist.");
        require(shares > 0, "Shares must be greater than 0.");

        FractionalToken storage fractionalToken = _fractionalTokens[tokenId];
        require(fractionalToken.totalShares >= shares, "Not enough shares available.");

        require(msg.value >= totalPrice, "Not enough Ether sent to buy the shares.");

        address seller = ownerOf(tokenId);

        CustomERC20 token = fractionalToken.token;
        uint256 allowance = token.allowance(seller, address(this));

        //require(allowance >= shares, "Not enough allowance to transfer shares.");

        token.transferFrom(seller, msg.sender, shares);
        payable(seller).transfer(totalPrice);
        fractionalToken.totalShares -= shares;
        _shareOwners[tokenId][seller] -= shares;
        _shareOwners[tokenId][msg.sender] += shares;

        // Re-approve the remaining allowance
        uint256 remainingAllowance = allowance - shares;
        approveContractForToken(tokenId, remainingAllowance, address(this)); // Call the function to re-approve the remaining allowance

        emit TokenBought(tokenId, msg.sender, shares);
    }

This is my context.js file that connects it with the front-end:

const ApproveShares = async (NFT, shares) => {
      try {
        const contract = await SmartContractConnect();
    
        const customERC20Address = await contract.customERC20Address();
        const customERC20 = new ethers.Contract(customERC20Address, CustomERC20ABI, contract.signer);
    
        const totalPrice = ethers.utils.parseUnits((NFT.price * shares).toString(), "ether");
    
        console.log("Approving shares:", {
          tokenId: NFT.tokenId,
          shares,
          totalPrice,
        });

        const transaction = await customERC20.approve(contract.address, totalPrice);
        await transaction.wait();
        const allowance = await customERC20.allowance(contract.signer.getAddress(), contract.address);
        console.log("Allowance set:", {
          allowance: ethers.utils.formatUnits(allowance.toString(), "ether"),
        });

        console.log("Shares approved for transfer");
      } catch (error) {
        console.log("Error while approving shares", error);
      }
    };
    

    //Create NFt
    const CreateNft = async (name, price, shares, image, description, router) => {
        if (!name || !description || !price || !image || !shares)
          return console.log("Data is Missing!");
      
        const data = JSON.stringify({ name, description, image });
        try {
          const added = await client.add(data);
          const url = `https://infura-ipfs.io/ipfs/${added.path}`;
          //new
          const tokenId = await createSale(url, price, shares);
          await ApproveShares({ tokenId, price }, shares);
        } catch (error) {
          console.log(error);
        }
    };

    //Create Sale
    const createSale = async (url, Price, shares, isResale, id) => {
      try {
        const price = ethers.utils.parseUnits(Price, "ether");
        const contract = await SmartContractConnect();
    
        const transaction = !isResale
          ? await contract.createToken(shares, price, url)
          : await contract.resellToken(id, price);
       
        //new
        const receipt = await transaction.wait(); // Get the transaction receipt
        const tokenId = receipt.events.find(event => event.event === "TokenCreated").args.tokenId; // Find the tokenId from the "TokenCreated" event
        console.log("Transaction Completed: ", transaction);

        return tokenId; // Return the tokenId      
        //await transaction.wait();
      } catch (error) {
        console.log("Error while creating sale: ", error);
      }
  };
 const BuyNFT = async (NFT, shares) => {
        try {
          const contract = await SmartContractConnect();
          const price = NFT.price*(shares)
          const totalPrice = ethers.utils.parseUnits(price.toString(), "ether");
      
          const transaction = await contract.buyToken(NFT.tokenId,totalPrice, shares,{
            gasLimit: 5000000,
            gasPrice: 10000000000,
            value: totalPrice
          });
      
          await transaction.wait();
          router.push("/profile");
        } catch (error) {
          console.log("Error while buying NFT", error);
        }
    };

1 Answer 1

1

Problem is a confusion may be.

token.transferFrom(seller, msg.sender, shares); in this line of code you are trying to send the ERC20 tokens from the seller of tokenId, I guess the tokenId is the NFT id so why would you be sending the ERC20 tokens from the seller of nft to the msg.sender, who can be anyone who sends the some Ether to the buyToken function. And that's where you are getting the 'ERC20: insufficient allowance' because if its the case that seller is the owner of a NFT who wants to sell his/her NFT, he wouldn't have approved the ERC20 token to anyone who calls the buyToken function, seller only approves the NFT token to your platform contract.

What I think you are trying to achieve in buyToken function is that, if a user wants to buy NFT he first needs to buy your custom ERC20 token from your platform with ether. In that case it should be not the NFT seller who sends the your custom ERC20 tokens to the buyer of NFT but it should be your platform contract that needs to call the transfer function of ERC20 contract to send the tokens to the buyer. and make sure your contract have that ERC20 tokens available.

4
  • Thank you for explaining it to me so easily! To be honest I'm struggling a little with the code to know how to do it correctly but I understand now how the flow should be. What I'm trying to achieve is that a buyer would enter the amount of share he would like to own and then buy them, can you help me with what to edit in the code? Thank you!
    – Abde
    Commented Apr 11, 2023 at 6:32
  • I'm not much familiar with NFT fraction. Conceptually speaking, it should work in the way that each NFT owner hold a defined amount against his/her NFT. And when he wants to sell his shares of NFT, the owner needs to approve the amount shares he wants to sell to the platform contract. that way you'll not face the error you are facing now. For that your contract code looks fine you just need to take a approval offchain(frontend) from the NFT owner by calling the approve function from you custom ERC20 smart contract. please accept the answer if it helps
    – Naveed Ali
    Commented Apr 11, 2023 at 7:03
  • The way I'm doing it right now is that one a user creates a new NFT, all his shares should be put for sale on the marketplace directly. I'm approving this through calling the ApproveShares() function on the createNFT() function. I think there's something wrong with that function right not according to what you are saying but I can't spot that, do you have any suggestions? sorry for all the questions I'm just so lost.
    – Abde
    Commented Apr 11, 2023 at 12:17
  • what represents the shares of NFT? are they in ERC20 tokens, if yes, then you will have to approve the the ERC20 tokens to your platform contract, so that it can send these to buyers as shares, on behalf of NFT owner. And to approve the ERC20 tokens you'll need to call the approve function of ERC20 contract not the one you are saying
    – Naveed Ali
    Commented Apr 12, 2023 at 3:50

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.