DEV Community

Mateus Ferreira
Mateus Ferreira

Posted on

How to create a resell token functionality in your NFT marketplace smart contract

This article is intended to be a continuation of @dabit3's great tutorial on how to create a NFT Marketplace on Ethereum with Polygon. So, if you haven't started from there, I suggest you do.

I'll be covering how to create a resell item feature to your marketplace, since this was an issue to me and to other developers to whom I have talked to.

First, in the NFT contract, we'll add a function to transfer the token. You may be asking why I don't just simply use ERC721's tranferFrom function. And the reason is that when I tested my functions, in some situations requests reverted with "transfer caller is not owner nor approved", even though msg.sender == ownerOf(tokenId) asserted true. So my solution was to write a custom transfer function. But feel free to share the solution for this strange bug in the comments if you know it. So this will be added to your NFT contract:

contract NFT is ERC721URIStorage {

    (...)

    function transferToken(address from, address to, uint256 tokenId) external {
        require(ownerOf(tokenId) == from, "From address must be token owner");
        _transfer(from, to, tokenId);
    }

}
Enter fullscreen mode Exit fullscreen mode

The "external" visibility allows only other smart contracts to call the function. And the "require" statement checks if the caller is the token's current owner.

Now, in the MarketPlace contract, you have to import the NFT contract to call the function, and it will look like this:

import "./NFT.sol";
Enter fullscreen mode Exit fullscreen mode

The second change I made was to add a "creator" attribute to the MarketItem struct, so it won't lose the information about who first minted the token between transfers:

struct MarketItem {
        uint256 itemId;
        address nftContract;
        uint256 tokenId;
        address payable creator;
        address payable seller;
        address payable owner;
        uint256 price;
        bool sold;
    }
Enter fullscreen mode Exit fullscreen mode

That means you'll have to alter your other existing functions and add this attribute too. I also created a new event, to broadcast that the token is being sold again:

event ProductListed(
        uint256 indexed itemId
    );
Enter fullscreen mode Exit fullscreen mode

Also, to write smaller functions and be able to reuse code, I created a modifier function to prevent that others than the contract owner do the operation:

    modifier onlyItemOwner(uint256 id) {
        require(
            idToMarketItem[id].owner == msg.sender,
            "Only product owner can do this operation"
        );
        _;
    }
Enter fullscreen mode Exit fullscreen mode

And finally, the function to (re)list the token in the marketplace:

function putItemToResell(address nftContract, uint256 itemId, uint256 newPrice)
        public
        payable
        nonReentrant
        onlyItemOwner(itemId)
    {
        uint256 tokenId = idToMarketItem[itemId].tokenId;
        require(newPrice > 0, "Price must be at least 1 wei");
        require(
            msg.value == listingPrice,
            "Price must be equal to listing price"
        );
        //instantiate a NFT contract object with the matching type
        NFT tokenContract = NFT(nftContract);
        //call the custom transfer token method   
        tokenContract.transferToken(msg.sender, address(this), tokenId);

        address oldOwner = idToMarketItem[itemId].owner;
        idToMarketItem[itemId].owner = payable(address(0));
        idToMarketItem[itemId].seller = oldOwner;
        idToMarketItem[itemId].price = newPrice;
        idToMarketItem[itemId].sold = false;
        _itemsSold.decrement();

        emit ProductListed(itemId);
    }
Enter fullscreen mode Exit fullscreen mode

For testing:

const { expect } = require('chai')
const { ethers } = require("hardhat");

let market;
let nft;
let nftAddress;
let marketAddress;

beforeEach(async ()=>{
    const Market = await ethers.getContractFactory("NFTMarket");
    market = await Market.deploy();
    await market.deployed();

    marketAddress = market.address;

    const NFT = await ethers.getContractFactory("NFT");
    nft = await NFT.deploy(marketAddress);
    await nft.deployed();
    nftAddress = nft.address;
})

describe("Marketplace", () => {
   (...)
    it("should allow buyer to resell an owned item", async () => {
    const [, creator, buyer] = await ethers.getSigners();

    await nft.connect(creator).createToken("www.mytoken.com")

    const listingPrice = await market.getListingPrice();

    await market.connect(creator).createMarketItem(nftAddress, 1, 100, {value: listingPrice})

    await market.connect(buyer).createMarketSale(nftAddress, 1, {value: 100})

    await market.connect(buyer).putItemToResell(nftAddress, 1, 150, {value: listingPrice})

    const item = await market.fetchSingleItem(1)

    expect(item.seller).to.equal(buyer.address)
    expect(item.creator).to.equal(creator.address)
  })
})
Enter fullscreen mode Exit fullscreen mode

And in the front-end:

export async function resellOwnedItem(id, price, signer) {
  const marketContract = new ethers.Contract(
    nftmarketaddress,
    Market.abi,
    signer
  );

  const listingPrice = await marketContract.getListingPrice();
  const tx = await marketContract.putItemToResell(
    nftaddress,
    id,
    ethers.utils.parseUnits(price, "ether"),
    { value: listingPrice.toString() }
  );
  await tx.wait();
}
Enter fullscreen mode Exit fullscreen mode

I'm taking the signer as an argument here but you can also fetch it in your function as @dabit3 did:

const web3Modal = new Web3Modal();
const connection = await web3Modal.connect();
const provider = new ethers.providers.Web3Provider(connection);
const signer = provider.getSigner();
Enter fullscreen mode Exit fullscreen mode

And this is how I handled this task. If you found a better solution, please share in the comments. Thank you!

Discussion (11)

Collapse
hotarumoon profile image
Ege

Thank you so much for the post, I have been trying to do this myself. Really appreciate it.

Collapse
latonet profile image
latonet

I get the message after adding your code to NFTMarket.sol ..... can't move forward with running the app. Any solution? Thanks

[{
"resource": "/C:/Users/admin/nft-marketplace/contracts/NFTMarket.sol",
"owner": "generated_diagnostic_collection_name#2",
"severity": 8,
"message": "Member \"transferToken\" not found or not visible after argument-dependent lookup in contract NFT.",
"startLineNumber": 209,
"startColumn": 9,
"endLineNumber": 209,
"endColumn": 36
}]

Collapse
mcmaurok profile image
mcmaurok

This is what I need to create a resell function, thanks Mateus. Could you upload the market.sol file? I'm getting an error when adding the "creator" attribute to other existing functions in the market.sol file and getting an error because of this. What functions did you add the "creator" attribute?

Collapse
mateusasferreira profile image
Mateus Ferreira Author

hey! there you go: github.com/mateusasferreira/nft-ma...
but I would take some parts of this code with a grain of salt as this is a work in progress (currently paused)

Collapse
mcmaurok profile image
mcmaurok

Yes, I will be verifying the code. Thank you so much I really appreciated it, just what I needed!

Collapse
izadgashb profile image
Crypto

I got the unhandled error
TypeError: marketContract.fetchItemsCreated() is not a function
Source: pages\creator-dashboard.js (31:38)
in this line:
const data = await marketContract.fetchItemsCreated()

Collapse
michaelblythe profile image
michaelblythe

Great work on this, I'm using this code and but seem to be getting the below error - any ideas? I get this when attempting to mint OR buy, using an EVM based chain where the contracts are successfully written to.

code: -32000
message: "gas required exceeds allowance or always failing transaction"
[[Prototype]]: Object
message: "Internal JSON-RPC error."
[[Prototype]]: Object

Collapse
livando profile image
Don Livanec

anyone getting this error when trying to upload an NFT to a test network?

message: 'execution reverted: ERC721: transfer caller is not owner nor approved'
Enter fullscreen mode Exit fullscreen mode
Collapse
kanishksharma8 profile image
KanishkSharma8

Hey! the code works well till the NFT is relisted for selling. But when I try to buy that relisted NFT it gives me this error..
code: -32603, message: 'Internal JSON-RPC error.'
data: {code: -32603, message: 'Error: Transaction reverted: function call failed to execute'}
Please help I have been stuck on it.. tried every solution there is.. Is there something that I'm missing?

Collapse
thalesbmc profile image
Thales Brederodes

Good job Mateus.

Amazing explanation!!

Collapse
silviarainicorn profile image
Silvia Barros

Hi Mateus!
I just wanted to thank you for the code. I think I have it working properly. Thanks a lot for sharing your solution!