If you read about NFTs for the first time, you might be under the impression that an NFT somehow stores a digital asset in the blockchain. In most of the cases, however, this is not true. If you have read my previous post, you will remember that in its essence, an NFT is nothing but a registry that maps assets, identified by a token ID, to its owners. So an NFT documents ownership, but does not even reference the asset itself in its basic form. But where is the asset itself? Answering this question is the purpose of todays post.
Before getting into how and where digital assets managed by NFTs are typically stored, let us briefly discuss the storage options that we have at our disposal.
First, there is the Ethereum blockchain itself. Using the Ethereum state as long-term storage has a number of remarkable advantages – the data, once stored, is essentially immutable, easy to access and almost impossible to delete. However, there is also a major disadvantage which, in most cases, is actually a show-stopper – it is incredibly expensive.
Let us see what incredible really means and do some math. Writing a 32 byte word to storage by running the
SSTORE opcode costs 20.000 units of gas. Thus 1 KB (1024 bytes, i.e. 32 words) cost 640.000 units of gas. The current gas price is at around 75 gWei, meaning that storing 1 kB would cost around 48 Mio. gWei, i.e. 0.05 Ether. At a price of 3000 USD / ETH, storing 1 kB of data would therefore cost roughly 150 USD. Storing a full digital asset, maybe an image, on-chain is therefore not really an option in most cases.
Alternatively, we could of course use any available centralized storage solution. This could be a web server that you or someone else operates, or one of the many available cloud-based storage solutions like Amazons S3, GCP storage or even Dropbox. These solutions are typically very affordable and might even have a free tier, and the data is backed up regularly and thus well protected against accidental data loss. These storage solutions are, however, centrally managed and operated. If someone mints an NFT and stores the digital asset that a token represents on a web server run on some AWS EC2 machine, there is a risk that this person either loses interest (or goes out of business) and simply shuts down the machine or deletes the asset at some point, or even manipulates the asset after the token has been minted. So data stored in these centralized solutions is typically not well protected against being tampered with and not durable.
Therefore, a third option is very popular in the NFT universe – decentralized peer-to-peer storage. In a previous post, we have already touched on IPFS. In addition to being decentralized, IPFS has the additional advantage of using content-based addressing. This simply means that the address under which data is accessible in the IPFS network is a hash of the data itself. If the data changes, so does the address, which implies that data stored in IPFS is effectively immutable – as long as you use the same address to access it, you can be sufficiently confident that the data is the same every time you access it. As a downside, however, the network can forget your data – everything that you store in IPFS will be distributed over many nodes, and a node can decide to drop your data in favor of data that is more frequently requested. Thus using IPFS to store real digital assets is only a good idea if you also find a node that pins your data (which, of course, again introduces a certain centralization).
In many cases, you will not simply want to store the asset itself, but maybe other data somehow associated with the image, like a human-readable description, the artist or other attributes. To support this, most NFTs use a two-step approach. The token itself contains a link to a file with metadata in JSON format. One of the fields stored in this file is then a link (or, more precisely, an URI) pointing to the actual image.
In this way, all we have to store on-chain is the URI of the metadata. We can then choose any of the storage options that we have discussed for the metadata and the actual image. Note, however, that these choices of course will have an impact on the integrity of the image and the metadata, we will discuss this in a bit more depth in the next section.
As this scheme is very common, it has actually been standardized as part of the ERC-721 standard. In fact, the standard defines an (optional) method
tokenURI which receives the token ID and returns a URI pointing to the metadata. The standard also proposes a scheme for the metadata, however, when we look at a few real-word example further below, we will find that none of them actually fully adheres to this standard – some leave out fields the description and most of them add non-standard fields like attributes.
The standard does not say anything about how the token URI is built from the token ID. It does, however, specify that the URI be unique, i.e. no two different token IDs should result in the same URI. A natural choice that we see in reference implementations is to use a base URI that is a property of the contract in combination with the token ID. This has the advantage that the token URI is automatically unique, and, assuming that the base URI is fixed over the lifetime of the contract, also stable. If the implementation allows a re-use of token IDs, this is especially useful as it avoids an attack where a token is burnt and then re-minted with the same ID but a different token URI (of course, such an attempt could be detected by listening on the created events, but could still be confusing for everybody who has stored the token ID).
Ensuring image integrity
How secure are the various storage options that we can use for metadata and image? If you place the metadata on a traditional, centrally managed web server, i.e. if you use a token URI like “http://my.site.com/token/…”, then, of course, whoever controls the domain “site.com” is able to change the metadata or even redirect queries to a different server. One approach to protect against this is to somehow add a cryptographic hash of the metadata to the token as well, so that this hash is stored on-chain, and can be used to validate the metadata.
This approach has been standardized in EIP-2477. This standard (which, actually, is only a draft at the time of writing) adds the method
tokenURIIntegrity which is supposed to return a hash value of the metadata and the algorithm used to create the hash. It appears, however, that this is not yet widely adopted, and in fact, none of the examples that we will analyze in the next section offers this method.
So if you really want to make sure that the metadata is not changed, you can either use this approach or a content-based medium like IPFS or a non-standard extension (the token URI could, for instance, contain a hash of the metadata even if it points to a traditional web server).
What about the image itself? The image URI is part of the metadata, but of course whoever controls the storage system could replace the image itself by some other content. Again, there are several options to mitigate this risk – you could use content-addressable storage for the image as well, add a cryptographic hash of the image to the metadata or add a cryptographic hash value of the image to the token data.
The choice is yours, and probably depends on the nature of the asset and the infrastructure which is at your disposal. As a guidance, however, it is of course helpful to see how existing contracts out there in the wild are approaching this, and this is what we will do now.
Real world use cases
To get an idea for some real-world use cases, let us head over to the OpenSea NFT market place and look at some NFTs. Our first example is the ArtBlocks collection. Technically, this is an ERC721 contract which implements a couple of additional methods, among them the methods of the metadata extensions. Let us take a look at one specific item in this collection, say #507 which has the token ID 21000507. We can use Etherscan to query the
tokenURI method for this token ID.
As a result, you should get the following URI back.
So the token URI is built from a base URI and the token ID (if you look at the source code of the contract, you will find that theoretically, the contract also allows to optionally use IPFS, but at least for this specific token, this option has apparently not been chosen). If you navigate to this URL, you will obtain a JSON-formatted file which contains a couple of attributes, like the artist, a description, a hash value and a link to the image.
Taking a look at the description on the project homepage tells us that the hash value is, in this case, not a hash of the image itself, but a seed from which the image can be recreated at any point in time. This seed is also stored on-chain, in a public mapping called
tokenIdToHash. As Solidity automatically generates getter functions for public variables, you can query this mapping using Etherscan and confirm that the hash string in the metadata and the entry in the mapping match. The same is true for the script itself, thus even though the image is stored at a central location, the data needed to reconstruct the image and verify its integrity is completely stored on-chain.
As a second example, let us look at the 0N1 Force collection. This is a collection of 7.777 randomly-generated virtual characters which are traded as an NFT. Again, this is an NFT according to the ERC721 standard that also realizes the metadata extension. This time, however, both metadata and image are stored on IPFS.
To illustrate this, we will use character 493 out of this collection. In order to retrieve the token URI, again head over to the contract on Etherscan, select the
tokenURI method, enter the token ID 493 and hit the “Query” button. As a result you should see
This is a link into the IPFS file system. To get the metadata behind, you either need to install an IPFS node yourself or – much easier – use one of the available web-based IPFS gateways. Let us use ipfs.io itself, which makes the content of the metadata we are after available at this URL. The metadata file which we obtain this way again contains a few attributes and an image link, again pointing to an IPFS CID which contains the actual image (be careful, accessing this link is slow and might result in HTML error 429 a few times). Thus we are in the scheme presented above – the CID for the metadata protects the integrity of the metadata, the metadata contains the CID of the image and therefore the integrity of the image is protected as well.
Finally, let us look at a more classical example – the “Now you are free” painting from the first Damien Hirst collection “Currency”. From a technical point, this again follows the same pattern – the contract has a
tokenURI method, querying this for the token ID 7813 returns a link to the IPFS metadata and this in turn contains a link to the actual image, both on IPFS. According to the official project homepage, however, each of the images initially exists twice – purely digital and as a physical painting. During a certain period of time after minting the NFT, the owner can then decide to either exchange the NFT for the physical painting – in which case the token will be burnt – or keep the token – in which case the physical painting will be destroyed.
This closes our post for today. In this post and the previous post, we have covered the ERC-721 token standard in sufficient detail to be ready to get our hands dirty and dive into the actual implementation. To implement an NFT, we will need some additional features of Solidity which we will discuss in the next post.