Once you learn about the basics of Ethereum based NFTs, you realize that your JPEG is not actually stored on the blockchain for most projects. The blockchain only holds a link, which points to a metadata file. That metadata file then points to your JPEG.
The NFT Image
It makes sense that the JPEG isn’t stored on chain, those images can be huge at high quality. Let’s look at a Bored Ape for example, randomly, the 61st most rare bored ape, #7678!
If we dive into the Bored Ape Yacht Club smart contract, and read the tokenURI
property for this pizza king crown guy, we find it points to ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq. This is the metadata for ape #7678 (shown fully below if you’re curious). The metadata points to an image stored at ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/7678.
Note: we can look at these ipfs urls in a traditional browser using the schema https://ipfs.io/ipfs/<CID>
The image (technically a PNG) is a 631x631 square at 255kb on disk.
According to the Ethereum yellow paper, which lays out the gas fee schedule, a 256 bit word costs 20k gas, or 625 gas for 1 byte (8 bits). That’s 640k gas for 1kb.
255kb image * 640k gas = 163.2m gas for the 255kb image.
So how do we calculate gas fees? The Ethereum docs lay it out prior to the London hard fork:
Total fee = Gas units (limit) * Gas price per unit
And after the London hard fork:
Total fee = Gas units (limit) * (Base fee + Tip)
So with London, instead of just Base fee (gas price per unit) we add in a tip. We’ll ignore the tip for our calculations, call it an approximation, and continue to use the pre-London formula.
Let’s do the math!
What is the price per unit of gas today (11/21/21)? Average has been about 90 gwei. This has fluctuated wildly based on congestion in the network, from 30 gwei up to thousands of gwei, when Shib is pumping or a super hot project is released. What is the cost for our image that will use 163.2 million gas units?
90 gwei per unit * 163.2m units = 14.688b gwei = 14.69 eth
We divide by 1 billion to convert gwei to eth, because 1 gwei is 1 billion wei (the smallest unit of Ether), and 1 eth is 1 billion gwei.
Woah. Can that be right? 14 fucking eth to store my image? How much is eth today? $4350 per eth!
14.69 eth * $4350/eth = $63,901
That seems excessive for a single image. And if the bulls on Reddit are right, Ethereum is headed to $10k per coin. Sounds right to me. Let’s see how accurate that is when we look back in a few years. In any case, it doesn’t seem worth gambling our project on the price of Ethereum. Ethereum would need either an exponential decrease in price, or a large decrease in gas cost, for it to ever make sense to actually store this image on chain as a PNG.
Worse, for BAYC, we need to store 10,000 images. This would cost $639 million. It appears storing the raw image directly in this way is not an option.
The NFT metadata
We pulled the metadata earlier for our super rare Bored Ape Yacht Club #7678, let’s take a look:
{
"image": "ipfs://QmXtfhV4LEGCRVvRRa788k5wN49rShvSv2yUgtgpJMfNsH",
"attributes": [
{
"trait_type": "Background",
"value": "Purple"
},
{
"trait_type": "Eyes",
"value": "Robot"
},
{
"trait_type": "Hat",
"value": "King's Crown"
},
{
"trait_type": "Fur",
"value": "Cheetah"
},
{
"trait_type": "Clothes",
"value": "Sailor Shirt"
},
{
"trait_type": "Mouth",
"value": "Bored Unshaven Pizza"
}
]
}
Not much! The metadata is a simple JSON string. It contains a link to the NFT’s image with 6 categories of attributes. Impressive in its simplicity. How much data is that?
344 bytes when we ignore whitespace. Now this seems like something we can store on chain. How much according to our calculations above?
625 gas/byte * 344 bytes = 215000 gas
90 gwei per unit * 215000 units = 0.01935 eth = $84.17
It’s a massive reduction in size compared to the image, but $84.17 per property sheet is still not cheap. For BAYC, at 10,000 NFTs for the contract, this would have been $840k. It’s amazing how things add up.
Clearly we should not store these files directly as strings. If anything, we will need a function that generates the strings within the smart contract. Now things start getting complicated.
Why Off Chain?
It should seem pretty obvious at this point why we would want to store these things off chain.
Cost - this is the core of most reasons for storing metadata off chain, and also the official reason given in the ERC-721 Spec metadata section:
Alternatives considered: put all metadata for each asset on the blockchain (too expensive)
Flexibility - you can deploy your contract once, point it at your metadata file, and never update your contract again. Any updates to the JPEG url or properties of the NFT can be done by updating the metadata file without any cost. If this was stored on chain, you would need to pay to write any changes to your contract.
Complexity - because you can’t just store these files raw on the blockchain, we need intelligent schemes to baked into our smart contract to generate the image or metadata tied to the NFT. See below for a project doing just that.
Risk - the logic in your deployed contract on chain is immutable. Many projects are hit by bugs in that logic. There is a risk that the bug cannot be fixed without redeploying your contract and forcing all users to migrate to a new contract, which they may or may not do. Many projects instead die this way. The more code on chain, the greater risk you’ve injected a project ending bug.
It makes perfect sense why we would store our assets and metadata off-chain, but is it possible to store your metadata on-chain? Certainly! The most interesting example I have found is from the Anonymice project. They abide by the ERC-721 spec, which requires metadata be stored in the NFT at the tokenURI
property which contains a string of JSON conforming to this schema:
{
"title": "Asset Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this NFT represents"
},
"description": {
"type": "string",
"description": "Describes the asset to which this NFT represents"
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
}
}
}
Which as you can see, the Bored Ape metadata schema gets close to matching as well.
How do the Anonymice do it? They generate a base64 encoded string of the metadata file at the tokenURI
, instead of a link to a JSON file on an IPFS server. Smart! You can read about their tactics on their blog, where it is very well explained.
They do need to be clever, because base64 encoding which will actually increase our size by about 33%. In fact, our Bored Ape PNG from above increases from 255kb to 340kb with encoding. So we cannot just store pure base64 encoded images unless we want to pay millions of dollars.
On Chain vs Off Chain?
So what should you do for your project? I love storing and doing as much on chain as possible. I personally start with everything on chain, with links to renders or shortcuts on IPFS as a practical solution. But as we know, IPFS is not forever. Ideally your source of truth can live on chain. It will take clever coding solutions to store these things on chain without making your contract deployment prohibitively expensive or complex.