In this next edition of the ongoing series covering the Lisk interoperability solution, we discuss a very useful blockchain object, the NFT (non-fungible token), and the new module dedicated to handling them. This topic was discussed in the presentation Lisk Interoperability: Token and NFT Standards given at Lisk.js.
In the previous blog post, we discussed fungible tokens, which are interchangeable and indistinguishable. Non-fungible tokens, on the other hand, are not interchangeable and any two NFT are always distinguishable. To give a non-blockchain analogy, fungible tokens are akin to regular currencies: only the amount of the currency matters, and not precisely which coins are used for the payment. NFTs are more akin to other possessions in that they identify a given, uniquely determined object (material or not). For example, any two houses are different objects and are not interchangeable, thus they would be tokenized using NFTs on a blockchain. This analogy illustrates the many potential use cases existing for NFTs; they can tokenize virtually everything from art, to housing. Furthermore, they could also be used for representing legal contracts or intellectual property rights.
Multiple resources exist describing the general use of NFTs, for example, NFTs on Wikipedia. In this blog post, we will not further discuss the prospects of NFT as a concept, but we will focus on how NFTs will be implemented in the Lisk ecosystem, and how the NFT module will make them easily accessible, and usable by any blockchain built with the Lisk SDK.
The NFT Module
In this section, we provide a brief overview of the features of the NFT module, specified in LIP 0052. The specifications are in many places similar to the ones for the token module, which was presented in our previous blog post: A New Token Module. The information below is non-exhaustive, and we would like to refer all readers to the LIP for additional details.
A dedicated NFT module is useful for several reasons. It provides blockchain developers that require NFTs in their applications with an easy-to-use, out-of-the-box, and straightforward solution. The NFT module defines a standard module store and exposes a set of functions to be used by sidechain developers, in order to interact with the NFT store in a safe and simple manner. Furthermore, it guarantees that the NFT format used by the multiple applications of the Lisk ecosystem are compatible with each other, and that NFTs can easily be transferred from one chain to another.
In the proposed module, NFTs are identified by a tuple of three integers: the chain ID of the chain where the NFT is created, the collection integer, and the index. This NFT ID is unique throughout the whole ecosystem.
The collection integer allows the chain to define multiple sets of NFTs, each with a different purpose in the application. For example, the Piracy module (which we define further below) defines one collection for regular pirates, one for captains, one for cartographers, and finally one for treasures.
The index of the NFT is automatically assigned to the next available index whenever the NFT is created.
A major difference between NFTs and fungible tokens is that NFTs can have specific attributes that are stored in the NFT module store. This property is a byte sequence that is not deserialized by the NFT module. Each custom module using an NFT collection should define schemas to serialize and deserialize the attribute property.
When NFTs are transferred cross-chain, the attributes are transferred with the NFT to the receiver and can be used and modified for custom logic. Meanwhile, the original NFT attributes are also saved in the native chain, and when the NFT is returned the attributes are compared, and specific logic can also be applied here.
NFT Module Store
All information regarding NFTs is stored as key-value pairs in the NFT module store. The NFT module store is separated into several substores serving different internal purposes. The only substores we will cover here are the NFT substore, and the user substore. The first one contains the owner, potentially the locking module and the attributes of all NFTs present on the chain. The second acts as a lookup table, allowing addresses to be mapped to the owned NFTs.
Figure 1: The NFT substore uses the NFT ID as the key and stores the NFT owner, the potential locking module ID, and the NFT attributes as the store value.
Figure 2: The user substore allows retrieving all NFTs owned by a given address.
Similarly to the Token module, the NFT module only implements the following two commands:
- The NFT transfer command: This is used to transfer NFT ownership from one address to another.
- The NFT cross-chain transfer command: This is used to transfer NFTs from one chain to another, and at the same time to modify the NFT owner.
These two commands allow users to send NFTs across the ecosystem without having to handle messages or understand the underlying interoperability protocol.
As is the case with fungible tokens, all NFTs are escrowed in the native chain. Technically, this means that when a token is sent from its native chain to another, the NFT is not erased, but the owner of the NFT now becomes the receiving chain. When an NFT is returned, the native chain can then check that the NFT is indeed coming from the chain it was sent to, and was not maliciously created and transferred from another chain. This implies that NFTs can only be transferred to and from their native chain.
Figure 3: This illustrates the rule above with the cross-chain transfer of NFTs from the A-chain. The NFTs can be sent back and forth from the A-chain to the B-chain (and also back and forth to the C-chain), however, NFTs from the A-chain cannot be sent from the B-chain to the C-chain.
The exposed functions are the entry gates to the NFT module store and how other modules interact with it.
A few examples of the exposed functions are listed below, and more can be found in the LIP-0052.
- initializeCollection is used to create a new type of NFT.
- create is used to issue new NFTs of a given collection.
- destroy is used to destroy NFTs of a given collection.
- lock is used to lock NFTs, in essence setting their lockingModuleID property and restricting their use until they have been unlocked.
- unlock is used to unlock previously locked tokens.
- transfer is used to update the NFT owner.
- transferCrossChain is used to move the NFT to another chain and update its owner.
- setAttributes is used to set or modify the attributes of an NFT.
The exposed functions are designed to always maintain a valid NFT module store, as they define the allowed state transitions that can be induced by other modules. They further simplify the usage of the NFT module by other modules, by providing simple interfaces that do not require knowledge of the module's inner workings.
The next section describes more elaborate use cases and shows the exposed functions in action.
Ahoy hearties! To exemplify the use of the NFT module, let's present Pirates of the Seven Chains, a blockchain game already showcased by one of our research team members, Alessandro during the last Lisk.js event: https://youtu.be/BTtLbhSgubA?t=834.
Pirates of the Seven Chains defines a new custom module: the Piracy module. This module uses 2 different NFT collections:
The interactions that pirates can have in the ecosystem are then specified in several custom commands.
The first command allows us to go down to the local tavern and hire a new pirate for our crew. It's called hirePirate and performs the following:
- Subtracts the hiring fee of the pirate from your balance (say LSK or a custom gold token). The hiring fee depends on the rank of the pirate.
- Triggers the create function to instantiate the new pirate and adds them to your crew.
Now what good is a pirate with no experience of the sea? The next command is sail and it sends the pirate to explore the world and gain experience. It performs the following:
- Takes a pirate and a chain ID as parameters.
- Checks if the given chain ID is already part of the pirate's visited chains (one of the pirate collection attributes). You don't increase your level for going to the same chain twice.
- If the chain is a new one, it increases the pirate's level and updates the pirate's visited chains. This is achieved by calling the setAttributes function of the NFT module with the updated attributes.
- Triggers the transferCrossChain function to send the pirate to the target chain.
Nota bene, if you simply use the cross-chain command to send pirates across the sea of interoperability, the pirate will be moved to the other chain. However, as this command merely moves the pirate without knowing anything about the pirate code, it will not increase the pirate's experience and level.
Lastly, once our crew is strong and experienced, it is time to visit other crews and plunder their treasures. This command does the following:
- Takes an array of pirates, a chain ID, and an address as parameters.
- The chain ID and the address correspond to the target of the plunder.
- Sends the pirate cross-chain to the target chain.
- Sends a custom plunder CCM to trigger the Pirate module plunder logic. This message would contain the following:
- The array of pirates participating in the plunder.
- The address of the crew that the plunder is targeting.
On receiving the plunder CCM, the following logic is triggered on the target chain:
- Checks that the whole attacking crew is still in the chain.
- Compares the strength of the 2 crews (the attacking and defending ones), by computing the sum of all pirate levels plus a rank bonus and a random number (which must be generated with on-chain information).
- If the attacker wins: he gets a treasure NFT from the defender's address.
- This is achieved by triggering the transfer function of the NFT module.
- If the defender wins: 1 attacking pirate is destroyed.
- This is achieved by triggering the destroy function of the NFT module.
- Send the remaining attacking crew back to their original chain, with their potential treasure.
- This is achieved by triggering the transferCrossChain function of the NFT module.
We can of course keep building on this and create new collections. For example, we can generate new types of treasures by calling the 2 functions: getNextAvailableCollection and initializeCollection.
This blog post has presented the NFT module and discussed its features and design choices. The NFT module provides the common layer and functionalities on which developers can start building. We hope this will inspire you to create a fantastic new blockchain application.
As tokens and NFTs are sent across the ecosystem, the question of a particular chain getting terminated and leaving the ecosystem must be answered. What happens to tokens and NFTs that were sent to a terminated chain? This topic will be presented in the next blog post in this series covering state and message recovery.
If you wish to dive further into the technical specifications or ask questions about the NFT module, we invite you to visit our Research forum, particularly the "Introduce NFT module" thread.