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

mateusasferreira

Mateus Ferreira

Posted on December 7, 2021

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!

💖 💪 🙅 🚩
mateusasferreira
Mateus Ferreira

Posted on December 7, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related