cover_nft_blog.png

Introduction

In our introductory NFT blog post, we explained what NFTs are and what they are used for. In the meantime, we delivered Suprabit Giant NFT to our business partners and employees and hopefully opened a new era of NFT corporate gifts đź‘Ť.
Suprabit Giant was a small but very interesting project that included a digital artist, full-stack and blockchain devs, a business developer, and a legal officer who jointly delivered a unique experience to our partners.

This post will help you get into the technical details of the Suprabit Giant implementation.

NFTs

As a quick technical introduction to NFTs, we will compare fungible and non-fungible tokens with simplified pseudocodes for the most basic functions, keeping track of the account balance, adding funds to the account, and removing funds from the account. In the examples below, fungible tokens are on the left, and non-fungible tokens are on the right. Notice how we don’t distinguish individual fungible tokens on the left - we store balance as a number, while with the non-fungible tokens on the right, the balance of an account is defined as a collection of token ids - each token has its unique id. Transferring fungible tokens means just decreasing the sender’s balance, and increasing the receiver’s balance. Meanwhile, transferring non-fungible tokens means removing specific tokens from the sender’s collection and inserting them into the receiver’s collection.

1// Account balance - integer
2account.tokens = 100;
3// Add 10 funds
4account.tokens += 10;
5// Remove 10 funds
6account.tokens -= 10;
1// Account balance - set of token ids
2account.tokens = [1, 3, 100, 200];
3// Add tokens with id 99
4account.tokens.insert(99);
5// Remove token with id 99
6account.tokens.remove(99);
Fungible TokenNon-Fungible Token

ERC721

Ethereum Improvement Proposals (EIPs) describe standards for the Ethereum platform, including core protocol specifications, client APIs, and contract standards. Among EIPs there are Ethereum Request for Comments (ERCs) which tackle application-level standards and conventions.

ERC721 introduces a standard interface for the implementation of an NFT smart contract. It defines a set of methods that each smart contract needs to implement to be ERC721 compliant. Below we can see the simplified ERC721 interface, this is not a full ERC721 interface, we only extracted a few basic functions.

1/// @title ERC-721 Non-Fungible Token Standard
2/// @dev See https://eips.ethereum.org/EIPS/eip-721
3///  Note: the ERC-165 identifier for this interface is 0x80ac58cd.
4interface ERC721 /* is ERC165 */ {
5    /// @notice Count all NFTs assigned to an owner
6    /// @dev NFTs assigned to the zero address are considered invalid, and this
7    ///  function throws for queries about the zero address.
8    /// @param _owner An address for whom to query the balance
9    /// @return The number of NFTs owned by `_owner`, possibly zero
10    function balanceOf(address _owner) external view returns (uint256);
11
12    /// @notice Find the owner of an NFT
13    /// @dev NFTs assigned to zero address are considered invalid, and queries
14    ///  about them do throw.
15    /// @param _tokenId The identifier for an NFT
16    /// @return The address of the owner of the NFT
17    function ownerOf(uint256 _tokenId) external view returns (address);
18
19    /// @notice Transfers the ownership of an NFT from one address to another address
20    /// @dev This works identically to the other function with an extra data parameter,
21    ///  except this function just sets data to "".
22    /// @param _from The current owner of the NFT
23    /// @param _to The new owner
24    /// @param _tokenId The NFT to transfer
25    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
26
27    /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
28    ///  TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE
29    ///  THEY MAY BE PERMANENTLY LOST
30    /// @dev Throws unless `msg.sender` is the current owner, an authorized
31    ///  operator, or the approved address for this NFT. Throws if `_from` is
32    ///  not the current owner. Throws if `_to` is the zero address. Throws if
33    ///  `_tokenId` is not a valid NFT.
34    /// @param _from The current owner of the NFT
35    /// @param _to The new owner
36    /// @param _tokenId The NFT to transfer
37    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
38}

But why is this even important for us? Can’t we create our own NFT smart contract without any fuss with standards? In theory, we can, no one forces us to be ERC721 compliant, in practice, a short answer - no. Now for the long answer, we need to enable interoperability between our NFT contract and other applications. A primary example of this interoperability is a wallet application. The general expectation is that users can see and manage their tokens in their wallet application - this is generally a mobile app. A wallet application can check which tokens the user owns only by reading the NFT smart contract directly. Now imagine what would happen if every NFT smart contract was implemented differently - a wallet application would need to develop an interface for each smart contract which doesn’t make sense. This is why we accept ERC721 and other EIPs and implement smart contracts accordingly. Any application can integrate with our NFT just by fetching the address of our smart contract as everything is publicly available.

Now that we have a better understanding of what we need to implement it’s a good time to introduce OpenZeppelin, an open-source library of standardized smart contracts that were developed following best security practices, and are extensively audited. Within the library, we have an implementation of the basic ERC721 compliant smart contract which tackles all the standard functionality of the NFT, minting, fetching balance, transferring, etc. As smart contracts are object-oriented, we can just inherit OpenZeppelin’s basic ERC721 contract and extend it with our business logic that is specific to our NFT.

At this point, we have a smart contract with basic NFT functionality - we have the tokens, but what differentiates them? Where is the uniqueness, the praised non-fungibility? This is where the token metadata comes into play, and if we consult the ERC721 interface we can spot the metadata extension which describes a way to attach metadata to the contract and each token individually. This allows us to attach some assets to tokens, like images, videos, and other media, or data in general. Below you can see the interface of the metadata extension. The main function here is the tokenURI which allows us to specify a distinct URI for a specific token. The idea is that the tokenURI points to a JSON file that contains all the metadata related to the specific token.

1/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
2/// @dev See https://eips.ethereum.org/EIPS/eip-721
3///  Note: the ERC-165 identifier for this interface is 0x5b5e139f.
4interface ERC721Metadata /* is ERC721 */ {
5    /// @notice A descriptive name for a collection of NFTs in this contract
6    function name() external view returns (string _name);
7
8    /// @notice An abbreviated name for NFTs in this contract
9    function symbol() external view returns (string _symbol);
10
11    /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
12    /// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC
13    ///  3986. The URI may point to a JSON file that conforms to the "ERC721
14    ///  Metadata JSON Schema".
15    function tokenURI(uint256 _tokenId) external view returns (string);
16}

Below is the example of the token metadata in JSON format, tokenURI should point to this data. In this example we modeled an NFT for a football player card, the metadata contains a URL to an image of a player, and a couple of attributes assigned to the token. Observant among you may have already noticed a link to an image hosted on the hns-cff.hr domain, oh no. It’s completely valid to question the integrity of our NFT at this point, we promised you full control of your NFT, but then you find that someone else has full control of the essential part of the NFT - the digital asset it encapsulates. What happens when the owners of the hns-cff.hr domain decide they don’t want to host the image anymore a few months or even a few years after you received the NFT? They have the full right to do so, they don’t have any incentive to continue hosting the image for free. The NFT without the digital asset becomes just an id assigned to you in the smart contract, practically useless.

1{
2    "name": "Luka Modric",
3    "image": "https://hns-cff.hr/files/images/luka-modric.jpg",
4    "attributes": [
5        {
6            "trait_type": "country",
7            "value": "Croatia"
8        },
9        {
10            "trait_type": "kit_number",
11            "value": 10
12        },
13        {
14            "trait_type": "position",
15            "value": "midfielder"
16        },
17        {
18            "trait_type": "captain",
19            "value": "true"
20        }
21    ]
22}

You might wonder why don’t just store all the metadata in the blockchain? Storing data in the blockchain is expensive, storing just 1MB of data to the Ethereum blockchain costs roughly 4 ETH which is, at the time of writing, equivalent to roughly $13,000. We won’t dig deeper into what is the reason behind this, but for anyone interested, a description of the Ethereum Virtual Machine (EVM) and its opcodes might be a good starting point. Nevertheless, don’t worry, we won’t stop here, there is a solution for the issue, this is where IPFS comes to save us.

IPFS

InterPlanetary File System is a protocol and peer-to-peer network for storing and sharing data in a distributed file system. It doesn’t sound like something that would solve our issues, but IPFS uses content-based addressing as opposed to location-based addressing which is a standard on the web today. If we look at the standard URL, let’s take an URL for the image of our football player as an example. Location-based addressing strictly defines on which domain is the image hosted, and where exactly is the image stored on that domain, in our example the domain is hns-cff.hr, and the image itself is located in the directory files/images/. Any change in either the domain name or the directory structure would result in this URL becoming invalid, alongside our NFT that depends on it.

URL Example

On the other hand, IPFS takes a completely different approach. As its name already says, content-based addressing addresses files based on their content. Imagine if the URL address for the image would be the hash of the image, it wouldn’t be convenient for humans as the URL wouldn’t be readable, and the links would be error-prone. But in our case we would greatly benefit from content-based addressing as we wouldn’t be vulnerable to any domain or directory changes, as long as someone has the original file, the URL can be resolved. Let’s try to check what would an IPFS URL look like. Content identifier (CID) is based on the hash of the file so for simplification, we can think of it just like a hash. A consequence of the CID is that any change in the file’s contents results in a different CID, which means that the file we are addressing with an IPFS URL is immutable. The file itself can be updated, but that would also update the CID so the original IPFS URL will point to the old version of the file. This is a great property for our use case, with IPFS CIDs we get addressing that preserves the integrity of the content.

IPFS URL Example

As IPFS is also a distributed file system, multiple peers in the network can host the same file, as the CID of the file would be the same there wouldn’t be any integrity violations. Multiple peers hosting the same file would only help others retrieve the file. If we try to retrieve the file from the peer that doesn’t have a file with that CID locally present, the peer will query neighboring peers if they have a file with that CID - this query propagates through the network until the file is found. A consequence of a peer-to-peer system is that the query for a file can take a long time to resolve as we need to propagate through the network until we find a peer that hosts this file.

Ok, now we know how to immutably address the metadata, but did we solve the issue if the original author stops hosting metadata? At least one peer in the IPFS still needs to host this file if we want NFT to work properly? True, at least one peer needs to host metadata for NFT to function, but now we at least have the ability to host data ourselves which is surprisingly easy as multiple cloud services offer us IPFS storage service - some of them even have free tiers that are more than sufficient to accommodate the storage of NFT metadata. This property comes from the fact that the metadata is not hosted by some server behind a domain, rather it’s hosted on IPFS, any IPFS peer should be able to find this file if at least one peer in the network hosts it.

You might be confused by some IPFS URLs looking like the example below. This link represents an HTTP gateway of an IPFS peer - ipfs.io in this example, it allows us to access IPFS files from the browser. You might wonder, didn’t we return to the beginning as we still need to use the HTTP gateway of some peer - what happens if that peer stops working? This is the exact reason why it’s strongly recommended to address IPFS files using ipfs:// URL format as this allows consumers of that link to use any peer they want to access that file. We can even specify a list of IPFS peers that we use, so we have alternatives if one peer stops working.

IPFS Peer Gateway

All in all, IPFS solves our issue of storing and linking metadata. Before deploying a smart contract we need to generate metadata, and media assets that will be linked with NFTs so that we can configure what the tokenURI method of the NFT returns. Below is the example of our metadata for one of our NFTs, we followed OpenSea’s metadata format.

1{
2    "name": "Suprabit Giant #0",
3    "description": "This Suprabit NFT represents one of 100 unique frames of Suprabit Giant digital artwork.",
4    "image": "ipfs://bafybeigp6cki2w7v26coqecxa5iohadhjg345n5b7ukkesbvq3kqeguvs4",
5    "animation_url": "ipfs://bafybeig77wew3xfhejgatekmah5t46btoeteugaa2maynicyctl4zxwp6m",
6    "external_url": "https://suprabit.eu/nft/giant/0",
7    "attributes": [
8        {
9            "display_type": "boost_number",
10            "trait_type": "Mobile",
11            "value": 5
12        },
13        {
14            "display_type": "boost_number",
15            "trait_type": "IoT",
16            "value": 10
17        },
18        {
19            "display_type": "boost_number",
20            "trait_type": "Blockchain",
21            "value": 4
22        },
23        {
24            "display_type": "boost_number",
25            "trait_type": "AI",
26            "value": 25
27        },
28        {
29            "display_type": "boost_number",
30            "trait_type": "Cloud",
31            "value": 9
32        },
33        {
34            "display_type": "boost_number",
35            "trait_type": "Table Tennis",
36            "value": 47
37        }
38    ]
39}

The next step is to pick a blockchain network to where the NFT smart contract will be deployed to. As we used ERC721 which is an interface for Solidity smart contracts, we need to choose a network that supports Ethereum Virtual Machine (EVM). Naturally, Ethereum is the first network to be considered, but at the time of writing Ethereum’s high transaction cost due to high network traffic is not suitable for our case. Polygon is a few orders of magnitude more efficient than Ethereum at the moment so it was an easy decision to select it over Ethereum.

Minting

Lastly, we need to figure out an easy and intuitive way for users to claim their gifts. This is a hard task as the blockchain ecosystem introduced a new way of interaction with online services which is fairly complex for new users. We have to anticipate that a majority of users won’t even have a crypto wallet so we need to guide them through the whole process, not just the interaction with our smart contract. We utilized WalletConnect to help us connect users’ wallet applications with our NFT website where users redeemed their gift NFTs. The last thing we needed to do is to actually create and assign NFT to the user, this process is called minting. In order to mint the NFT, we need to call the smart contract and pass the token id we want to mint, and the address of the owner’s wallet. This operation is packed into a transaction that will require a small fee in order to be executed on the blockchain. As NFT is a gift we wanted to handle all minting fees ourselves which is why we created a small minting service that mints NFT at the user’s request - of course, we had to protect this service against unauthorized requests.

Along the way, we stumbled upon a hurdle of connecting a user’s wallet to the Polygon network which at the time of publishing NFT had to be manually configured in the wallet application, and this hurt the user experience a bit. Fortunately, this won’t be the issue for so long as there is already a standard proposal that should fix this issue - the wallet applications just need to implement support for it.

Conclusion

To wrap it up, we successfully launched our first NFT and delivered it as a gift to our business partners and employees. It was awesome to hear positive feedback regarding the user experience of redeeming the NFT as we invested a lot of time to make it as easy, and as intuitive as possible. The general idea behind the gift was to give people first-hand experience with blockchain and introduce them to the concept of NFTs and help spark their ideas for NFT use cases.

If you have any questions regarding this project, or if you are interested in various NFT use cases, and where can it be used in your field of work - feel free to contact us and let us give it thought together.


andro.png
Written by
Andrijan Ostrun

Software Engineer

If you like this article, we're sure you'll love these!