What data does an NFT actually store?

I’m working on an NFT marketplace using Solidity and OpenZeppelin’s ERC-721 contract as a foundation. Each NFT has five key attributes: id, image hash, description, collection name, and the creator’s address, where the image hash is retrieved from IPFS upon upload.

I’m unsure about the best way to manage this data. Currently, I have an Image struct that contains all these attributes, and I am saving them in an array. When I mint a new NFT, I use the id from the Image struct and the creator’s address. This setup means that essential data is stored outside of the ERC-721 contract itself.

This raises my question: what exactly defines an NFT if its crucial attributes are housed in a separate struct rather than with the NFT?

Is this method correct? Does the ERC-721 standard only offer fundamental token functions, while the metadata is stored elsewhere?

pragma solidity ^0.5.0;

import "./ERC721Full.sol";

contract TokenMarket is ERC721Full {
  string public contractName;
  Asset[] public tokens;
  uint public assetCounter = 0;
  mapping(uint => bool) public _tokenExists;
  mapping(uint => Asset) public assets;

  struct Asset {
    uint tokenId;
    string ipfsHash;
    string title;
    string category;
    address payable creator;
  }

  event AssetMinted(
    uint tokenId,
    string ipfsHash,
    string title,
    string category,
    address payable creator
  );

  constructor() public payable ERC721Full("TokenMarket", "TMARKET") {
    contractName = "TokenMarket";
  }

  function createAsset(string memory _hash, string memory _title, string memory _category) public {
    require(bytes(_hash).length > 0);
    require(bytes(_title).length > 0);
    require(bytes(_category).length > 0);
    require(msg.sender != address(0));

    assetCounter++;

    assets[assetCounter] = Asset(assetCounter, _hash, _title, _category, msg.sender);

    require(!_tokenExists[assetCounter]);
    uint _tokenId = tokens.push(assets[assetCounter]);
    _mint(msg.sender, _tokenId);
    _tokenExists[assetCounter] = true;

    emit AssetMinted(assetCounter, _hash, _title, _category, msg.sender);
  }
}

Any feedback on improving this code would be great. I’m new to Ethereum development.

You’ve got the basics right. ERC-721 handles ownership and transfers, but metadata storage is flexible - that’s by design. Your approach is solid. The NFT is basically the token ID plus its metadata, doesn’t matter where the metadata lives.

I used a similar pattern in my first marketplace project. Your Asset struct approach beats pure tokenURI implementations. On-chain metadata guarantees availability and you don’t have to worry about IPFS nodes going down or someone messing with your metadata.

There’s a bug in your minting logic though. You’re pushing to the tokens array and using that length as tokenId, then calling _mint with that value. Problem is your assetCounter starts at 1 but array indices start at 0. Just call _mint(msg.sender, assetCounter) directly instead of using the array push return. The tokens array is redundant anyway since you’ve got the assets mapping indexed by counter.

For production, figure out if the extra gas costs for on-chain metadata are worth it versus the usual tokenURI approach that points to IPFS JSON files.

yeah, you’ve got it right. nft tokens only store ownership data - which wallet owns which token id. all the actual stuff like images and descriptions gets stored separately, either on-chain like you did or off-chain through ipfs links. most big projects use tokenURI pointing to json metadata files since it’s cheaper on gas, but your approach works great for smaller collections.

Oh interesting question! I’ve been messing with NFTs myself and this confused me at first too.

Your approach is pretty standard. ERC-721 handles ownership and transfers - who owns token #123 and how to move it. The metadata, images, and descriptions can live anywhere.

Looking at your code though - why both the tokens array AND assets mapping? You might be duplicating data. Also your _tokenId from the push might not match assetCounter since arrays are 0-indexed but you start your counter at 1.

Here’s what I’m really wondering - how do wallets and marketplaces find your metadata? Most expect a tokenURI function returning a URL (usually ipfs://) to a json file with the metadata. You planning to implement that?

Since you’re storing everything on-chain in structs, have you thought about gas costs? Each mint’s gonna be expensive compared to storing a URI pointing to ipfs metadata.

What made you store metadata on-chain vs having tokenURI point to ipfs json files like most projects? Genuinely curious about your reasoning!