Intermediate

 

First contracts – NFTs

 

Welcome to our tutorial on First contracts – NFTs. In this tutorial, we will explore the basics of Non-Fungible Tokens (NFTs) and how to create your first NFT contract on the Tezos blockchain network. NFTs are unique, indivisible digital assets that can represent ownership of a digital item such as an in-game item, a digital art, or a collectible. We will walk through the process of creating, deploying, and managing NFTs on the Tezos network. Additionally, we will look at best practices for using NFTs on the Tezos network and how to interact with them. Whether you are a developer, researcher, or simply a blockchain enthusiast, this tutorial will provide valuable insights and hands-on experience in working with NFTs on the Tezos blockchain network.

What is a smart contract?

A smart contract is composed of mostly three elements that are maintained by nodes in the state of the blockchain:

  • Their balance: a contract is a kind of account, and can receive and send Tez

  • Their storage: data that is dedicated to and can be read and written by the contract

  • Their code: it is composed of a set of entry points, a kind of function that can be called either from outside of the chain or from other contracts.

The code of smart contracts is expressed in Michelson, a Turing-complete stack-based language that includes common features as well as some very specific blockchain-related features:

  • It doesn’t have variables, but can manipulate data directly on a stack, through a set of stack manipulation instructions. For example, the ADD instruction consumes two elements from the top of the stack and puts their sum on top of the stack.

  • It is strongly typed, with basic types such as integers, amounts of tez, strings, and account addresses, as well as pairs, lists, key-value stores (big-maps), or pieces of code (lambdas).

  • It has limited access to data, and can only read data from its own storage, data passed as parameters during calls to its entry points, and a few special values such as the balance of the contract, the amount of tez sent to it during a call, and the creation time of the current block. It can also access a table of constants.

When someone calls a smart contract, all the contract can do can be summarized as:

  • Performing some computations.

  • Updating the value of its own storage.

  • Generating a list of operations. These operations will be performed once the contract’s own execution is over. This can include transferring funds to other accounts, calling other contracts, or even originating new contracts, but not much more.

One key property of the execution of smart contracts on Tezos, designed to reduce the risk of bugs, is that if any part of the execution of a contract itself, or in the call of any other contract it calls, generates an error, then everything is canceled, and the result is as if the initial call to the contract had never been done. The idea is that either everything goes as intended, or nothing happens at all, reducing the risk of unintended situations.

Smart contract languages

The Michelson language is a bit like the equivalent of the assembly language or even the machine code of regular computers. It is a low-level language that can do anything using a small set of relatively simple instructions but is not very easy for humans to read and write.

A number of high-level languages are available on Tezos, that allow developers to write smart contracts in very expressive, easy-to-read forms: SmartPy, Archetype, or Ligo are the main examples. All these languages give access to all of Michelson’s smart contracts features but use different philosophies that fit the style of all kinds of developers.

Here is an example of a very basic smart contract that does a simple computation and stores the result.

Contract expressed in the Archetype high-level language:

archetype example

variable data : int = 0

entry compute(param : int) {
  data := 2 * data - 3 * param;
}

The same contract, compiled to Michelson:

 
storage int;
parameter (int %compute);
code { UNPAIR;
       PUSH int 3;
       MUL;
       SWAP;
       PUSH int 2;
       MUL;
       SUB;
       NIL operation;
       PAIR };

Properties to keep in mind

As a resident of a decentralized blockchain, a smart contract has some unique properties:

  • Eternal: it stays available forever!
    as long as enough people keep maintaining the blockchain.

  • Immutable: no-one can ever change its code
    but the effect of the code depends on the data in its storage, which may change.

  • Decentralized: its existence and availability don’t depend on any third party
    but it can use data provided by third parties.

  • Public: anyone can read it, call it, and access its data,
    but it may contain encrypted data, and restrict access to its features.

A single NFT smart contract

You may have heard about NFTs: Non-Fungible Tokens. An NFT can be summarized as a digital asset that is uniquely identifiable, contains some information (metadata), has an owner, and can be transferred.

Digital information such as text, images, or music can usually be duplicated at virtually no cost, with no way of distinguishing the “original” version from mere copies. A blockchain such as Tezos provides a way to create a digital asset that stays unique: the data itself may be copied, but a unique, unambiguous owner is digitally attached to it and has full control over what happens to it. Note that the ownership of an NFT of a digital asset such as an image doesn’t usually imply ownership of the image itself and associated rights.

One of the key features of Tezos that makes it very suitable for NFTs, is the on-chain governance that doesn’t rely on forks. Indeed, consider what happens to the uniqueness of an NFT if the blockchain that hosts it was to fork. On Tezos, there is always an unambiguous way to identify which is the official blockchain, and therefore which is the true NFT.

Many artists, game producers, or brands select Tezos for their NFTs, from Ubisoft with the Quartz project using Tezos NFTs to represent in-game collectibles, to RedBull Racing which proposes F1-themed collectibles for their fans. A number of marketplaces like Objkt, Hic et Nunc, and Rarible allow people to find, buy or sell NFTs.

Question: based on what you have learned so far about what a smart contract is, take some time to think about how you would build a simple one to create your own NFT.

Looking at our summary above, an NFT will need three pieces of information:

  • a unique identifier
    we can use the identifier of our smart contract itself: its address.

  • some metadata
    we can simply put a string in its storage.

  • an owner
    we will put the address of an account in the storage.

We could deploy a smart contract that just has storage with two values: the metadata and an owner. But we wouldn’t be able to transfer it.

For this, we will need to add some code: a transfer entry point, that does two things:

  • Verify that the current owner is the one requesting the transfer

  • Replace the owner in the store with a new address.

Within a smart contract, we simply have access to the address of the author of the call, the caller or sender, with the guarantee that all verifications have already been done. For the transfer of our NFT, we simply need to include in our entry point, a single line of code that checks that this sender is the current owner of the NFT.

 

Storage

Entry points effects

 owner: address

 metadata: string

 transfer(newOwner)

 Check that the caller (sender) is the current owner

 Replace owner with newOwner in the storage

 

Here is the corresponding implementation in the SmartPy language:

import smartpy as sp

class Account(sp.Contract):
    def __init__(self, firstOwner, metadata):
        self.init(owner = firstOwner, metadata = metadata)

    @sp.entry_point
    def transfer(self, newOwner):
        sp.verify(sp.sender == self.data.owner, "Not the owner")
        self.data.owner = newOwner

In SmartPy, we generate a smart contract with a class that inherits from sp.Contract. Here, its constructor __init__ takes two parameters:

  • the address of the initial owner of our NFT

  • the content of the metadata.

self.init is then called, to put these two values in the initial storage of the contract.

In the transfer entry point, we can see how self.data.owner is used both to read and write the value of the owner to and from the storage.

Trust without a third party

A very powerful benefit of blockchains, and of smart contracts in particular, is that they provide a mechanism to bring guaranteed trust into a transaction, replacing the need for involved parties to trust each other, and the need for introducing a third party.

This is done by making the exchange atomic: either the two steps of our transaction succeed, or neither of them happens.

This takes advantage of the property we presented earlier, that any error in the execution of a smart contract, causes everything previously done during this execution to be canceled as if it had never happened.

The previous version of our smart contract already included the transfer of the NFT, assuming that the payment was done off-chain or through a separate transaction. To ensure the atomicity of the exchange, we need the same entry point to manage both the transfer of the item and the transfer of tez.

Four new features of Tezos are needed:

  • We need a new data type, tez, to represent the price.

  • We need the buyer to send some tez. On Tezos, a call to a contract is a special type of transaction, and you always send a number of tez (potentially 0) to a contract, when you call its entry point. That amount is automatically added to the balance of the contract.

  • We need to check that the transferred amount is correct. The code of an entry point has access to the amount sent by the caller, usually through an amount keyword.

  • We need to send that amount to the seller. We will use an instruction that generates a new transaction, from the balance of the contract to a destination address.

We need two entry points in our contract:

  • setPrice, for the seller, to indicate at what price they are willing to sell their NFT.

  • buy, for the buyer to initiate the purchase.

 

Storage

Entry points effects

  owner: address

  metadata: string

  price: tez

 setPrice(newPrice)

 Check that the caller is the owner

 Replace price with newPrice in the storage

 buy()

 Check that the amount sent is equal to the price

 Create a transaction that transfers price tez to the owner

 Replace owner with the caller

 

Here is the corresponding implementation in the Archetype language:

archetype nftForSale(owner : address, metadata: string)

variable price : tez = 0tz

entry setPrice(newPrice : tez) {
  called by owner
  effect { price := newPrice; }
}

entry buy() {
  require { r1 : transferred = price }
  effect {
      transfer price to owner;
      owner := caller;
  }
}

Note that with this version of our contract, if the buyer doesn’t immediately call setPrice after buying this NFT, nothing stops anyone from buying it from them at that same price.

By this, you complete this workshop successfully!!