NFTs - The Technical Internals

 

NFTs

The Technical Internals

If one hasn’t heard of NFTs,Non Fungible Tokens, one  could be admonished for living under a rock.  In the  last few years, we have seen the entry of NFTs into the day-to-day vernacular as a proxy for the hype surrounding cryptocurrencies and their adjacent products such as  Krypto Kitties and Bored Apes.  The association between those products and NFTs has become so strong that one could be forgiven for interchanging a Bored Ape Yacht Club image and the NFT behind it as one and the same.  In reality, the association is much less strenuous than imagined.  The more appropriate relationship between a Bored Ape image and its corresponding NFT is akin to associating Uber and Lyft with an internal combustion engine.  


So what exactly is an NFT and how does it work, and what do Bored Apes, Krypto Kitties, and the NFT hype have to do with each other?


I’m going to start off by prefacing that this article is going to be somewhat technical. Yes, we are going to look at some pieces of code, and yes, we will get into the machinery of what makes an NFT work, but this is necessary in order to convey what exactly is going on under the hood.  


The original blockchain solution, Bitcoin, looked at two pieces of information: wallets and coins.  A wallet is a virtual address on the blockchain to which coins can be sent and into which coins can be deposited.  Each such movement of coins from one wallet to another wallet is referred to as a transaction on the blockchain, and the aggregate history of all transactions is the ledger.  Over time the Bitcoin protocol accommodated some additional information that could be stored alongside each transaction, but the amount of information and how sophisticated that storage of information is was quite limited. In practice, for day-to-day transactions, Bitcoin has turned out not to be very useful.  


In response to both the potential power of digital transactions as well as the challenges that Bitcoin faced, Ethereum emerged as an alternative contender with a core premise of providing a programmatic layer to each transaction. What this means is that a transaction could be more than just the movement of digital coins from one wallet to another. It could also be the movement of data which in itself could be a piece of code that is executed on the blockchain. In order to enable such movement, a new programming language was developed, Solidity, and an execution environment was created, the EVM, Ethereum Virtual Machine.  To put it another way, Ethereum was able to disconnect the implementation of how tokens function on the blockchain from the underlying mechanics of how the blockchain worked. In contrast, Bitcoin functions with tokens and the blockchain as tied at the hip.  


The original token proposal was called ERC20 and represented a token name, symbol, and initial supply of coins. This information is defined in the constructor method of a contract.  The contract that implements this standard  at a bare minimum will look something like this:


contract TestCoin is ERC20 {
  constructor() ERC20("A Test Coin", "TEST") {
    _mint(msg.sender, 10,000,000,000);
  }
}


What this small piece of code does is define a new coin, TestCoin which inherits from the ERC20 type. In this TestCoin’s constructor, the contract calls the parent classes’ constructor with a short description of the coin and a token symbol. Once a unique coin type has been constructed, then the new coin receives an initial supply of coins. The msg.sender value is the wallet address that is minting this coin, and the value will be the initial number of coins that will be stored in the balance field of the contract. This is the initial supply. Depending on how the coin is defined, this supply can grow, decrease, or never change.  Future transactions that operate on this coin type, such as the TEST coin, will merely move this numerical value from one wallet to another, decrementing from the original wallet’s balance and incrementing on the destination wallet’s TEST coin and any other wallet downstream.


One important piece of information to keep in mind is that the constructor() method is invoked only once during the initial minting of the coin on the blockchain. If one was to redeploy this coin, then effectively one will be creating a new contract that if the name stays the same can be thought of as being a V2 of the original.  These two coins, though sharing the same name, are in fact different from the perspective of Ethereum and one can’t interchange between them.  


These coins conforming to the ERC20 standard are what is known as “fungible”. When one moves a TEST coin from Wallet A to Wallet B, those coins are indistinguishable from each other because they are effectively just numerical values tied to a unique coin type.  Those coins are  effectively the same, TEST can be used anywhere that TEST is accepted, regardless of which wallet it comes from.  


This then leads us to NFTs, Non Fungible Tokens.  


ERC20 was already a big step forward over Bitcoin because it enabled two key features:


  1. The ability to create domain specific coins with that coin’s own supply.

  2. The ability to write code in the Solidity language to manage the transfer of coins during a transaction based on code that was written.  


The combination of these two features enabled the creation of whole new lines of business applications and unleashed the early excitement around Ethereum. However, there was one limitation, there was no notion of uniqueness behind the tokens.  This then led to the creation of the ERC721 standard.  The big initial innovation behind the ERC721 standard was the idea that one could associate a Counter that would move up with each newly minted token, thus creating a uniquely identifiable token by pairing the ID of the contract that created the token along with the Counter of the token which looks something like this:


{0x123ABC, 456}  // Contract ID, Token Counter Pair


This unique pairing can then act as the non fungible element. Even if two items seemingly look alike, have the same balance, store the same data, associate themselves with the same token id, they can nevertheless be different entities because their counters are different.  Put another way, if one wanted to limit the supply of bored apes, and have only five copies of a particular bored ape, that can be enforced by validating that no more than five counted objects point to the same bored ape.   


Additionally, one new feature that ERC721 introduced is the concept of storing additional token metadata in a remote URI. It is then possible to set a URI on the newly created token which points to additional data about the token such as a bored ape image, a purchase agreement for a house, a lease on a car, or a watch warranty. The reason why storing this information on the blockchain or within the contract may be undesirable is because storage of data on the blockchain is relatively expensive. The data must be replicated across all nodes and exist in perpetuity. If one wanted to store Apollo13 as an NFT, storing the many GBs worth of video data on the blockchain can easily prove to be cost prohibitive. Additionally, the blockchain was not meant for nor designed to then act as a means of streaming and delivering this type of content. One could attempt to build such an application, but it would be highly unwise, akin to taking a Ferrari to Home Depot to haul bags of mulch, fertilizer, and gardening equipment. Such a feat is doable, but of questionable decision making.  


Let's take a closer look at what this might look like using our example above, modified to support the ERC721 standard.  


contract TestNFT is ERC721URIStorage, Ownable {
  using Counters for Counters.Counter;
  Counters.Counter private _tokenId;
  mapping (uint256 => string) private _tokenData;

  constructor() ERC721("Test NFT Token", "T_NFT") {}

  function createNewNFTToken(
    address tokenOwner,
    string memory tokenData;
    string memory tokenMetadataURI) public onlyOwner returns (uint256) {
      _tokenId.increment();
      uint256 newTokenId = _tokenId.current();
      _tokenData[newTokenId] = tokenData;
      _safeMint(tokenOwner, newTokenId);
      _setTokenURI(newTokenId, tokenMetadataURI);
    return newTokenId;
  }
}


This contract has some more sophistication, but not very much. It creates a new token type in the constructor. The interesting part is in the createNewNFTToken function, which can only be executed by the contract owner, the wallet that created the contract in the first place. This is a very important level of safety because otherwise, anyone with access to the blockchain can execute the code inside of createNewNFTToken without permission. The code inside the method is then broken into a few steps; increment the counter, store any data we are interested in living on the blockchain in a mapping from the new token id to the data, and mint the actual token, transferring it to the desired owner address. Finally, once the token has been minted, set the URI on the token as was discussed above so that information that shouldn’t live on the blockchain, but is relevant to the value of the token, can be accessed by future consumers and applications utilizing the token. The method concludes by returning the ID of the token that was just minted.  


A key idea to note is that these are “non fungible” tokens, not “non-immutable” (yes, the double negative is intentional). What this means is that although we can identify a token uniquely, the information stored inside of the token can change and evolve over time. Just as we wrote data at the time of minting, we can modify and mutate this data in the future, including the URI associated with the token. It is therefore very important to make sure that the data that is stored inside of the contract is kept private and that any functions implemented on the contract to interact with the contract’s data must be protected against malicious engagement.  



Comments