Non-fungible Tokens
A non-fungible token (NFT) is a globally unique token representing ownership of any distinct asset. Non-fungible tokens are implemented in Stardust as a standalone output type called NFT Output. Once minted, the NFT Output gets assigned a unique NFT ID by the protocol based on the minting transaction. Issuers may choose to define immutable properties on the output upon minting, such as:
- the issuer identity, namely the address of the issuer,
- metadata treated as binary data by the protocol.
A distinct feature of L1 NFTs is the ability to function as standalone wallets. Each NFT owns an address derived from its
unique NFT ID, called NFT Address. NFT addresses look pretty much like regular Ed25519 addresses when encoded in bech32,
except they always start with
the character z
after the separator. (iota1z...
, smr1z...
).
If NFT ID is a protocol generated value, how can funds locked under the NFT Address be unlocked in a transaction? The trick is to prove ownership of the NFT Output that defines NFT ID and hence NFT Address. To unlock an output locked to NFT Address, the owner has to include the NFT itself in the transaction. If the unlock of the NFT output is valid, it means that the owner authorized the transaction, therefore it may consume further outputs locked under that NFT Address.
Once minted, only the owner of the NFT Output may unlock it to include it in transactions. When an NFT Output is unlocked in a transaction but the output side doesn't contain it, the NFT gets burned. Any funds locked under its address are lost forever, since it is impossible to recreate the same output with the same NFT ID. Always check if the NFT owns something before burning it!
NFT Outputs support all possible (mutable) Features and Unlock Conditions, just like Basic Outputs. Therefore, you can define timelocks, expiration and storage deposit return conditions on NFTs, but also features like sender, metadata or tag. With such a powerful feature set, NFTs become first-class citizens in the ledger and also support smart contract chain interactions.
NFTs as native tokens?
The alert reader might be wondering: can't we just set the maximum supply of a native token to one to have a unique native token, therefore an NFT? The answer is yes, we could do that, but this approach has its limitations compared to the output based approach:
- NFTs link the actual owned asset via metadata to the token. Metadata of a native token sits in the foundry output, while the native token itself may reside in any other output. Therefore, the NFT and its metadata are detached from each other.
- The owner of the NFT as a native token has no control of the foundry, therefore it is impossible to melt the native token without the involvement of the issuer. The owner may still burn it, but then the storage deposit of the foundry can never be refunded.
- Native tokens can not own other asset in the ledger.
Minting an NFT
Transaction A displays the minting of an NFT on protocol level. For the sake of simplicity, we only define an immutable issuer and metadata upon minting, but it would be possible to add any unlock conditions or mutable feature as well.
Minting must respect the following constraints:
- NFT ID must be zeroed out. The protocol will replace it with the blake2b-256 hash of the Output ID upon booking.
- Issuer address must be unlocked on the input side,
- Immutable Metadata length must not exceed Maximum Metadata Length defined in TIP-22 (IOTA) or TIP-32 (Shimmer).
It is recommended to use one of the IRC standards to define NFT metadata. See IRC-27 for instance on how to define basic metadata linked to the NFT.
Transferring NFT
Transaction B sends the newly issued NFT Output #1 to the recipient.
- NFT ID is all zeros in NFT Output #1, since it has just been minted in Transaction A. While the value of the field (explicit NFT ID) is all zeroes, based on the protocol rules we know that the implicit NFT ID can be calculated from the Output ID of NFT Output #1.
- When the minted NFT is transferred for the first time, NFT ID must be set as the implicit NFT ID of the mint output. Therefore, NFT Output #2 sets the value of the NFT ID field as the blake2b-256 hash of NFT Output #1 ID. If you miss this step and leave it as all zeroes, the protocol interprets the transaction as the burning of the minted NFT and the creation of a new NFT.
- Notice, that the immutable features are not allowed to change. Their values are carried together with the NFT until it is burned.
- To change ownership of the NFT, unlock the address of the Address Unlock Condition (owner) in the transaction and set a new one (recipient) on the output side.
- Without further unlock conditions, owner loses the 100i storage deposit which is now controlled by recipient. When an NFT is sold via a marketplace, it is the platform that decides how to handle the storage deposit, whether the buying price accounts for it or the owner should be refunded by the recipient via a Storage Deposit Return Unlock Condition.
Why can't NFT ID be set directly upon minting?
It was shown in Transaction A that NFT ID must be zeroed out on minting, and via Transaction B that it has to be set to the protocol generated value in the first transfer. The reason for this is the chicken and egg situation rooted in how unique values are generated on protocol level:
Unique NFT ID is computed from Output ID, that is computed from the minting transaction content. The transaction content contains the value of the NFT ID field of the output. On minting, the NFT ID is set to zero to signal the minting operation. As soon as the transaction is prepared (don't even need to submit it yet) the Output ID is known. Therefore, NFT ID can be computed locally, but it is not possible to place it inside the minting output, as that would alter the transaction content, hence the Output ID and the NFT ID itself.
Transferring NFT with storage deposit return
Transaction C shows a conditional transfer of an NFT. By defining a Storage Deposit Return Unlock Condition and an Expiration Unlock Condition, the recipient has to claim the NFT transfer in a transaction that:
- Refunds the storage deposit to the owner address,
- within the timeframe specified in the expiration condition.
Should the recipient fail to claim the transfer in time, the ownership of NFT Output #3 falls back to the owner address defined in the Expiration Unlock Condition. An expired output can be unlocked by this address without having to fulfill the Storage Deposit Return Unlock Condition.
Transaction C defines that recipient has to claim the output in a transaction such that:
- owner is refunded with 100i via a Basic Output,
- the claiming transaction may only be carried out until May 24 2022 18:00:00.
Claiming a conditional NFT transfer
Transaction D shows how recipient can claim the conditional transfer initiated in Transaction C:
- recipient has to fund the transaction with the to-be refunded storage deposit, therefore Basic Output #2 is unlocked on the input side.
- NFT Output #4 removes the additional unlocks from the NFT and places it solely into the ownership of recipient.
- Basic Output #3 refunds owner with the storage deposit defined in the Storage Deposit Return Unlock Condition of NFT Output #3.
- Transaction D is only valid if the confirming milestone has a timestamp earlier than the one defined in the Expiration Unlock Condition of NFT Output #3.
- Since both NFT Output #3 and Basic Output #2 are unlocked by recipient address, it is enough to sign the transaction once in Signature Unlock #1 and reference this unlock in Reference Unlock #2.
Burning an NFT
The owner of the NFT may choose to burn it to retrieve the storage deposit of the NFT Output. Burning on protocol level simply means that the NFT Output is unlocked in a transaction, but it doesn't appear on the output side.
Transaction E shows how to burn an NFT and claim its storage deposit into a Basic Output.
Before burning an NFT, always check whether its NFT Address owns something in the ledger. Otherwise, you may lose those funds forever!
NFT as a wallet
Each NFT has its own unique NFT ID that also functions as an address on protocol level. The address can be deterministically generated from the ID as outlined in TIP-31, basically prepending NFT ID with the NFT Address Type and encoding it as a Bech32 string.
What this means is that any output (except for a foundry) can be locked to an NFT Address in the protocol. An NFT Address is a first class citizen on protocol level, just like an Ed25519 Address.
To unlock outputs locked under an NFT Address, the owner of the NFT must also unlock the NFT Output that defines it, hence proving ownership of the address. Note, that this is a significant difference compared to Ed25519 Addresses, where ownership is proved via a signing the transaction with a private key
Unlocking funds owned by the NFT
Transaction F shows how one can unlock an output locked to an NFT Address. Notice, that Basic Output #4 contains not only base tokens, but also native tokens. An NFT Output supports holding these native token directly in the output itself, but it would also be possible to place them in any output after Basic Output #4 is unlocked. It is the decision of the owner of the NFT to define what outputs to create in the transaction.
Just like any other output, an NFT Output supports holding up to Max Native Token Count (defined in TIP-22 for IOTA and TIP-32 for Shimmer). In case you need to store more native tokens, distribute them in Basic Outputs that are owned by the NFT.
Transferring NFT owned by the NFT
So far in the examples the NFTs were always owned by a private key backed address, therefore they were unlocked via a signature. What happens when an NFT is owned by another NFT?
Similarly to Transaction F, the NFT that owns the other one must be unlocked in the transaction to prove the ownership. Transaction G shows how the "owner NFT" can unlock the "owned NFT" and transfer it to a new recipient.
It is possible to introduce circular ownership with NFT Addresses (and also with Alias Addresses). In such a case, NFT A is owned by NFT B, while NFT B is owned by NFT A. This would mean that unlocking them in a transaction doesn't require a signature, therefore anyone could unlock them that leads to race conditions on protocol level.
To prevent this undesired property, the protocol forces an NFT Unlock to only be able to reference a previous Unlock. As a result, circular ownership becomes a deadlock, as neither NFTs can be unlocked anymore.
Wallets and applications should implement mitigation strategies to prevent a deadlock: always perform circular ownership checks before sending an NFT to another NFT!
NFT Collections
NFTs rarely exist in isolation, usually they are part of a bigger set, a collection. In smart contract based NFTs like ERC1155, a collection and the NFTs in it are all tracked in a single contract. In UTXO based systems like IOTA/Shimmer however this is not possible, so a new way of linking together NFTs is needed.
Stardust protocol allows linking NFTs together via the Issuer Feature. Since NFTs are first class citizens in the protocol, they can function as issuers themselves. This feature is exploited for L1 NFT collections.
Creation of a Collection NFT
A Collection NFT is a just a normal NFT Output but with special purpose: it is used to mint all NFTs in the collection. The Collection NFT becomes the Issuer of the NFT Outputs representing NFTs within the collection.
It is possible to:
- Permanently lock the Collection NFT to prevent any future minting. No diluting is ever possible by issuers.
- Lock the Collection NFT for some time to prevent minting,
- Deposit the Collection NFT into a L2 chain where minting activity can be governed via smart contracts or DAOs.
Transaction H mints a Collection NFT the same way as Transaction A mints a regular one. The metadata makes it clear that the intended use of this NFT is to serve as a Collection NFT.
- name defines the name of the collection,
- uri points to a website with more information about the project. Note, that type defines the resource type for uri.
Minting NFTs within the collection
NFTs within the collection must be minted in a way that their issuer is the Collection NFT. Therefore, we include NFT Output #10, the freshly minted Collection NFT from Transaction H in Transaction J, and define the Issuer Feature of all minted NFTs to hold the NFT Address of the Collection NFT.
We also place unique metadata in each NFT within the collection. The metadata is formatted according to IRC27 and contains information about:
- where the asset represented by the NFT resides,
- the issuer or artist,
- the collection the NFT belongs to,
- optional royalty addresses,
- and custom attributes.
Transaction J mints a very limited collection, there are only 3 items NFT Output #12, #13 and #14. The issuer must also provide the storage deposit for the newly minted NFTs, therefore Basic Output #7 is consumed in the transaction.
Once the minting transaction confirms, it is possible to fetch all NFT outputs within the collection via the Indexer API TIP-26. The following query returns all NFT outputs (their Output IDs) that have been issued by the Collection NFT:
GET <indexer-base-url>/api/indexer/v1/outputs/nft?issuer=<collection-nft-address>
Locking Collection NFT
The best way to ensure scarcity of collections is to prevent future minting activity. It is possible to lock the Collection NFT in the ledger for some time via a Timelock Unlock Condition, or for eternity by sending it to the zero address.
The zero address is an Ed25519 address where the hash of the Ed255129 public key is all zeroes, therefore there exists no private key that could successfully unlock it.