Logo
blank Skip to main content

Tezos Token Standards: Practical Examples of Implementing FA1.2 and FA2 Tokens

Tokenization is a game changer in the blockchain world. People can tokenize all kinds of assets, from traditional currencies to real estate and even artworks. The challenge is to tokenize these assets while making ownership management flexible and easy.

In this article, we talk about the specifics of asset tokenization in Tezos, the types of tokens this platform supports, and token standards it enforces. We discuss key differences between FA2 and FA1.2, the two key token standards in Tezos, and analyze their implementation based on a practical example.

This article will be useful to blockchain professionals exploring token implementation options on Tezos.

The need for token standardization

In this article, we discuss tokens as crypto assets and not native cryptocurrencies. As crypto assets, tokens are units of value that blockchain-based organizations or projects develop on top of existing blockchain networks. Tokens are multifunctional by their nature, allowing you to tokenize almost anything.

Take the gaming industry. In video games, while a player pays real money for in-game items, they donโ€™t get ultimate ownership over these items. For instance, the gameโ€™s developers can ban a playerโ€™s account and thus restrict the playerโ€™s access to all purchased items. However, with tokenized in-game items, once a player pays for an item, they become its true and only owner. Furthermore, since the transaction history is fully recorded on the blockchain and canโ€™t be tampered with, the ownership over a particular asset becomes easy to prove and validate, solving the problem of item duplication and misuse.

With the help of tokens, you can digitize anything valuable in the material world and trade it as cryptocurrency. For example, there are tokens tied to other currencies, like ETHtz and USDtez. At the same time, you can tokenize valuable possessions like pieces of art and copyrights using non-fungible tokens (NFTs).

Fungibility is the property of something being interchangeable with other similar assets or goods, and it is one of the criteria commonly used to distinguish between different types of tokens:

 types of tokens

Related services

Blockchain Consulting and Development Services

However, working with different types of tokens can be challenging for developers. You can write a custom smart contract with rich token processing functionality. But if other contracts and decentralized applications (dApps), like marketplaces or exchanges, donโ€™t know how to interact with it, your contract may be useless.

One way to solve this problem is by writing a separate smart contract that implements a unique approach for dApps to interact with your contract. However, it will be more efficient to follow a unified approach when creating, issuing, and deploying new tokens on particular platforms. Thatโ€™s what token standards are for โ€” they set common rules that ensure smooth interactions between different contracts operating with tokens.

Each blockchain platform has its own set of token standards, and in this article, we discuss the key token standards of the Tezos blockchain.

Read also:
Tezos Blockchain and Smart Contract Overview

Tezos token standards overview

As a relatively new blockchain platform, Tezos draws attention from both developers and businesses. Recently, Ubisoft joined Tezos as a corporate baker. In collaboration with Ubisoft, Tezos has introduced fully-decentralized NFT smart contracts. Also, three Swiss companies โ€” Crypto Finance AG, InCore Bank, and Inacta โ€” have announced that they will jointly use the Tezos blockchain to tokenize regulated assets using the DAR-1 token standard, which is reportedly based on the Tezos FA2 standard.

Tezos supports multiple smart contract languages and has a robust NFT ecosystem. It continues to scale through EVM- and WASM-compatible features, making it a versatile platform for various types of decentralized applications. 

In June 2023, Tezos deployed its 14-th upgrade, known as Nairobi, which has increased transaction speeds by up to eight times. This upgrade is a significant milestone in Tezos’ journey toward scalability and efficiency. 

In 2023, Tezos is hosting its Premier Developer Conference, Tez/Dev, focusing on harnessing Tezos for success. The conference aims to bring together core development teams to discuss recent and future developments, including scalability features set to come with the Mumbai upgrade, targeting 1 million transactions per second.

Token standards available in Tezos are described in the Tezos Interoperability Proposal (TZIP) documents that you can find in the platformโ€™s repository on GitLab.

In June 2023, the Tezos protocol was successfully upgraded, contributing to the development and growth of Tezos in a collaborative environment globally.

Among other TZIPs, token standards are identified as Financial Applications (FA). There are three completed versions of the FA Tezos token standards:

  • FA1 (TZIP 5 Abstract Ledger). Now deprecated
  • FA1.2 (TZIP 7 Approvable Ledger). Last updated on June 29, 2023
  • FA2 (TZIP 12 Multi-Asset Interface). Last updated on July 10, 2023

While the FA1 standard is already deprecated, the other two standards have been successfully implemented in SmartPy and LIGO. For more details about the current statuses and implementation references of these token standards, see Table 1 below.

Table 1: Tezos token standards overview

TZIPTitleCreation DateStatusLIGOSmartPy
TZIP-005FA1 โ€” Abstract Ledger12 April 2019Deprecatedโ€”โ€”
TZIP-007 FA1.2 โ€” Approvable Ledger20 June 2019FinalFA1.2SmartPy IDE – FA1.2 
TZIP-012  FA2 โ€” Multi-Asset Interface24 January 2020FinalFA2SmartPy IDE – FA2
TZIP-021DeFi Token Standard15 June 2023DraftTZIP-21SmartPy IDE โ€“ TZIP-21

Now, letโ€™s compare Tezos standards and take a closer look at each of them.

FA1 โ€” Abstract Ledger

FA1 was the first Tezos token standard. This standard was basically a minimalist version of the ledger, hence its name. The main purpose of FA1 was to map identities to balances and provide interactions with fungible assets for contract developers, libraries, client tools, etc.

In contrast to the object-oriented programming paradigm, thereโ€™s no mandatory inheritance between Tezos token standards, even though the FA1 standard contains the word โ€œabstractโ€ in its name. All subsequent standards donโ€™t have to be compatible with FA1.

FA1.2 โ€” Approvable Ledger

The FA1.2 standard is a symbiosis between the FA1 standard and the EIP-20 standard used in Ethereum. The key mechanism of the FA1.2 standard is the ability to approve the spending of tokens from other accounts. However, this standard can only be used for fungible tokens.

When implementing a token based on this standard, you must include all of the following entry points in its interface:

Table 2: Mandatory interface entry points of the FA1.2 standard

ParametersName
(address :from, (address :to, nat :value)) transfer
(address :spender, nat :value)approve
(view (address :owner, address :spender) nat)getAllowance
(view (address :owner) nat)getBalance
(view unit nat)getTotalSupply

FA1.2 doesnโ€™t prohibit developers from extending the token contract with additional functionality. For example, the SmartPy template of FA1.2 has additional entry points for minting and burning tokens, governance management, and so on.

FA2 โ€” Multi-Asset Interface

The FA2 standard is the most recent Tezos token standard. 

How does FA1.2 differ from FA2? Itโ€™s important to understand that FA2 canโ€™t be seen as a direct heir to FA1.2 for several reasons:

  1. In contrast to the FA1.2 standard, FA2 supports multiple assets, enabling the coexistence of different types of tokens such as fungible and non-fungible. In Ethereum, the same possibility is implemented in the EIP-1155 multi-token standard.
  2. The FA2 standard issues token transfer permissions differently than FA1.2. In FA2, permission can be granted using the update_operators entry point. In the FA2 specification, an operator is an address that can create transactions on behalf of the owner (whose address already stores tokens).
  3. The interface of the FA2 standard includes the following necessary entry points:

Table 3: Interface entry points of the FA2 standard

ParametersName
(list :transfer
  (pair
    (address :from_)
    (list :txs
      (pair 
        (address :to_)
        (pair
          (nat :token_id)
          (nat :amount)
        )
      )
    )
  )
)
transfer

(pair :balance_of

  (list :requests
    (pair
      (address :owner)
      (nat :token_id)
    )
  )
  (contract :callback
    (list
      (pair
        (pair :request
          (address :owner)
          (nat :token_id)
        )
        (nat :balance)
      )
    )
  )
)
balance_of
(list :update_operators
  (or
    (pair :add_operator
      (address :owner)
      (pair
        (address :operator)
        (nat :token_id)
      )
    )
    (pair :remove_operator
      (address :owner)
      (pair
        (address :operator)
        (nat :token_id)
      )
    )
  )
)
update_operators
(pair (address :owner) (nat :token_id))getBalance
(nat :token_id) (nat :supply)total_supply
() ((list nat))all_tokens

TZIP-21 โ€” DeFi Token Standard

TZIP-21 is the latest addition to the Tezos token standards, specifically designed for decentralized finance (DeFi) applications. This standard aims to provide a unified interface for DeFi tokens, making it easier for developers to create financial products on the Tezos blockchain. TZIP-21 includes features like interest-bearing tokens and flash loans, among others. It’s currently in the draft stage and is expected to be finalized later this year.

In addition to the existing token standards, Tezos continues to innovate in other areas. In Q2 2023, Tezos made significant strides in its rollup infrastructure roadmap, introducing WASM rollups. This development aims to enhance the scalability and efficiency of the Tezos blockchain. Additionally, Tezos introduced the Tezos Ecosystem DAO, aimed at managing and distributing XTZ to support community initiatives. These advancements signify Tezos’ commitment to fostering a robust and versatile blockchain ecosystem. In July 2023, Tezos introduced new technical features such as Data Availability Layer (DAL) and Etherlink. These features were revealed at TezDev 2023 and are expected to further enhance the blockchain’s capabilities. 

The Ithaca2 upgrade introduced a new consensus method called Tenderbake, enabling faster finality (verification of transactions). Additionally, the Granada upgrade brought liquidity baking to Tezos, laying the foundation for improved scalability.

Now that we know all the key aspects of the existing standards in Tezos, it’s time for the practical part. Since the FA1 standard is already deprecated, we will focus our attention on the other two standards โ€” FA1.2 and FA2 โ€” and write a smart contract that can interact with both of them.

Read also:
How to Implement a Custom Blockchain for Your Business: Graphene Framework

Writing a marketplace smart contract for Tezos

Letโ€™s create a universal smart contract that allows you to buy and sell an asset, regardless of the token standard. Weโ€™ll use SmartPy for writing this smart contract, since it can be compiled in Michelson.

Note: In order to reduce the code volume and simplify the smart contractโ€™s logic, we identify sales by IDs.

Here are the entry points that define the main business logic of our smart contract:

  • (A) registerMarket (tokenAddress, tokenType) โ€” registers a new market (token) that users will interact with
  • (A) removeMarket (tokenAddress) โ€” removes the market
  •  sellAsset (tokenAddress, tokenId, amount, price) โ€” puts your own asset up for sale and assigns a unique ID to this sale
  • cancelSale (saleId) โ€” cancels the sale; you can only cancel your own sale or one you have admin-level access to
  • buyAsset (saleId) โ€” enables you to buy an asset with a certain saleId; for the sale to take place, you must also pass the number of mutez that matches the price specified when calling sellAsset ()

Note: (A) means that only the admin can send this entry point.

The business logic of our marketplace includes three main stages:

  1. The marketplace admin registers new markets.
  2. One user account puts up a token for sale (remembers the ID of the sale).
  3. The second account, which is going to buy the token, specifies the ID of the sale and pays the mutez according to the price.

Hereโ€™s what it looks like from the adminโ€™s and the userโ€™s points of view:

 smart contract logic on Tezos

As the result of a successful sale, the token will go through the following transfer path:

 token sale path

In addition to the business logic, the contract also has governance logic, which restricts access to the registerMarket and removeMarket entry points for non-admin users. Tezos offers formal verification methods for added security in smart contracts.

This logic can be described in a separate class that will look like this:

Python
class Governance(sp.Contract): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def __init__(self, administrator): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.data.administrator = administrator 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private(with_storage="read-only") 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def isAdministrator(self, sender): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ return sender == self.data.administrator 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private(with_storage="read-only") 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def verifyAdministrator(self, sender): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ assert self.isAdministrator(sender), "TIOF_NOT_ADMIN" 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.entry_point 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def setAdministrator(self, administrator): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ sp.cast(administrator, sp.address) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.verifyAdministrator(sp.sender) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.data.administrator = administrator 

The code above followed the self-documentation approach; therefore, instead of magic numbers, a list of constants was given:

Python
@sp.module 
def main(): 
โ€ฏ โ€ฏ FA_1_2_TOKEN_TYPE = sp.nat(0) 
โ€ฏ โ€ฏ FA_2_TOKEN_TYPE = sp.nat(1) 

The business logic calls for making token transactions between addresses. For this reason, we created the generic TransferTokens class:

Python
class TransferTokens(sp.Contract): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private() 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def transferFA2(self, sender, receiver, amount, tokenAddress, id): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ ... 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private() 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def transferFA12(self, sender, receiver, amount, tokenAddress):  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ ... 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.entrypoint 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def transferTokenGeneric(self, params): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ sp.cast(params, sp.record(sender = sp.address, receiver = sp.address, amount = sp.nat, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏtokenAddress = sp.address, id = sp.nat, tokenType = sp.nat))  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ if params.tokenType == FA_2_TOKEN_TYPE:  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.transferFA2((params.sender, params.receiver, params.amount, params.tokenAddress, params.id)) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ else:  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.transferFA12((params.sender, params.receiver, params.amount, params.tokenAddress))

Now the utility functionality is ready. Letโ€™s move on to the Marketplace contract:

Python
class MarketPlace(Governance, TransferTokens): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def __init__(self, administrator): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ Governance.__init__(self, administrator) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ TransferTokens.__init__(self) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ # key - token address. Records consist of the type of token (FA1.2 or FA2) and a set of active sales 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.data.markets = sp.cast(sp.big_map(), sp.big_map[sp.address, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ sp.record(tokenType = sp.nat, sales = sp.set[sp.nat])]) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.data.saleCounter = sp.nat(0) # counter for saleId generation 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.data.sales = sp.cast(sp.big_map(), sp.big_map[sp.nat, # key - saleId 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ sp.record(tokenAddress = sp.address, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ tokenId = sp.nat,  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ amount = sp.nat, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ price = sp.mutez, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ seller = sp.address)]) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.entrypoint 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def registerMarket(self, params): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ sp.cast(params, sp.record(tokenAddress = sp.address, tokenType = sp.nat)) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.verifyAdministrator(sp.sender) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.verifyMarketNotExists(params.tokenAddress) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.data.markets[params.tokenAddress] = sp.record(tokenType = params.tokenType, sales = sp.set()) 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.entrypoint 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def removeMarket(self, tokenAddress): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ sp.cast(tokenAddress, sp.address) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.verifyAdministrator(sp.sender) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ for saleId in self.data.markets[tokenAddress].sales.elements(): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.transferBackTokens(saleId) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ del self.data.sales[saleId] 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ del self.data.markets[tokenAddress] 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.entrypoint 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def buyAsset(self, saleId): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ sp.cast(saleId, sp.nat) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.verifySaleExists(saleId) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ assert sp.amount == self.data.sales[saleId].price, "TIOF_PRICE_MISMATCH" 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.transferTokens(sp.record(sender=sp.self_address(), 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ receiver=sp.sender, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ amount=self.data.sales[saleId].amount, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ tokenAddress=self.data.sales[saleId].tokenAddress, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ id=self.data.sales[saleId].tokenId)) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ sp.send(self.data.sales[saleId].seller, self.data.sales[saleId].price) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.removeSale(saleId) 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.entry_point 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def sellAsset(self, params): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ sp.cast(params, sp.record(tokenAddress = sp.address, tokenId = sp.nat, amount = sp.nat, price = sp.mutez)) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.transferTokens(sp.record(sender=sp.sender, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ receiver=sp.self_address(), 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ amount=params.amount, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ tokenAddress=params.tokenAddress, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ id=params.tokenId)) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ saleId = self.makeSaleId() 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.data.sales[saleId] = sp.record(tokenAddress = โ€ฏparams.tokenAddress, tokenId = params.tokenId, amount = params.amount, โ€ฏ price = params.price, seller = sp.sender) 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.data.markets[params.tokenAddress].sales.add(saleId) 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.entrypoint 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def cancelSale(self, saleId): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ sp.cast(saleId, sp.nat) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.verifySaleExists(saleId) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.verifyAdminOrSeller(sp.record(sender = sp.sender, saleId = saleId)) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.transferBackTokens(saleId) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.removeSale(saleId) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private(with_storage="read-only") 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def verifySaleExists(self, saleId): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ assert self.data.sales.contains(saleId), "TIOF_NOT_EXISTENT_SALE" 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private(with_storage="read-only") 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def verifyMarketNotExists(self, tokenAddress): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ assert not self.isMarketExistent(tokenAddress), "TIOF_ALREADY_REGISTERED" 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private(with_storage="read-only") 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def verifyMarketExists(self, tokenAddress): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ assert self.isMarketExistent(tokenAddress), "TIOF_NOT_EXISTENT_MARKET" 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private(with_storage="read-only") 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def verifyAdminOrSeller(self, sender, saleId): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ assert (self.isAdministrator(sender) or (sender == self.data.sales[saleId].seller)), "TIOF_NOT_ADMIN_OR_SELLER" 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private(with_storage="read-only") 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def transferBackTokens(self, saleId): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.transferTokens(sp.record(sender=sp.self_address(), 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ receiver=self.data.sales[saleId].seller, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ amount=self.data.sales[saleId].amount, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ tokenAddress=self.data.sales[saleId].tokenAddress, 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ id=self.data.sales[saleId].tokenId)) 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private(with_storage="read-only") 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def transferTokens(self, sender, receiver, amount, tokenAddress, id): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ params = sp.record(sender = sender, receiver = receiver, amount = amount, tokenAddress = tokenAddress, id = id, tokenType = self.getTokenType(tokenAddress)) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.transferTokenGeneric(params) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private(with_storage="read-only") 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def isMarketExistent(self, tokenAddress): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ return self.data.markets.contains(tokenAddress) 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private(with_storage="read-only") 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def getTokenType(self, tokenAddress): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ return self.data.markets[tokenAddress].tokenType 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private(with_storage="read-write") 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def removeSale(self, saleId): โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.data.markets[self.data.sales[saleId].tokenAddress].sales.remove(saleId) 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ del self.data.sales[saleId] 
โ€ฏ โ€ฏ โ€ฏ  
โ€ฏ โ€ฏ โ€ฏ โ€ฏ @sp.private(with_storage="read-write") 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ def makeSaleId(self): 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ self.data.saleCounter += 1 
โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ โ€ฏ return self.data.saleCounter 

Now the code of our marketplace smart contract is ready and itโ€™s time to check how it interacts with FA1.2 and FA2 tokens.

Read also:
Smart Contract Security Audit: Penetration Testing and Static Analysis

Analyzing smart contract interactions with tokens

To see how our smart contract interacts with different types of tokens, letโ€™s analyze three tokenization use cases and present them in unit tests:

  1. Ounces of silver. These assets are fungible, so both token standards are suitable for this use case. In our example, we use the FA1.2 standard.
  2. Collection of gallery masterpieces. Artworks canโ€™t be called interchangeable, and the gallery is not limited to one masterpiece. For this use case, we can only use NFTs and the FA2 token standard.
  3. An ICO platform. On this platform, anybody can create a product (a concert, art, etc.) and look for investors willing to support it. The product is tokenized and investors can buy tokens in order to invest in that product. Once the productโ€™s life cycle has concluded, tokens can be sold back for a share of the profits. This platform entails the creation of various types of tokens. If you use FA1.2 here, you will have to create a new contract for every new product, which is quite expensive and inconvenient. In addition, there may be a need for NFTs. Therefore, the FA2 standard is better suited.

SmartPy has a built-in scenario unit testing tool that you can use to check a smart contractโ€™s performance. Another option is to deploy contracts and analyze them manually, but this method is less convenient than unit testing. 

Now letโ€™s create three tokens in a test scenario and mint the initial balances.

Read also:
NFT for Business: Use Cases, Benefits, and Nuances to Consider

A fungible FA1.2 token for ounces of silver

Weโ€™ll start with creating a fungible FA1.2 token for operating with ounces of silver.

Python
@sp.add_test(name = "MarketPlace test")
def test():
        scenario = sp.test_scenario(main)
        scenario.table_of_contents()
        admin = sp.test_account("Administrator")
        alice = sp.test_account("Alice")
        bob   = sp.test_account("Bob")
  
        # display accounts:
        scenario.h1("Accounts")
        scenario.show([admin, alice, bob])
       
        scenario.h1("Contracts initialization")
        scenario.h2("MarketPlace")
        marketPlace = main.MarketPlace(administrator = admin.address)
        scenario += marketPlace
     
  
        scenario.h2("FA1.2 token - fungible silver ounces")
        token_metadata = {
            "decimals"    : "0",
            "name"        : "ounces of silver",   
            "symbol"      : "Silver",              # Silver ounce
            "icon"        : 'https://smartpy.io/static/img/logo-only.svg'
        }
        contract_metadata = {
            "" : "ipfs://QmaiAUj1FFNGYTu8rLBjc3eeN9cSKwaF8EGMBNDmhzPNFd",
        }
        fa12 = FA12.FA12(
            admin.address,
            config = FA12.FA12_config(support_upgradable_metadata = True),
                token_metadata      = token_metadata,
                contract_metadata   = contract_metadata
        )
        scenario += fa12
  
        scenario.h3("Initial Minting for Alice")
        fa12.mint(address = alice.address, value = 50).run(sender = admin)

The FA1.2 token is ready, and the admin has minted 50 ounces of silver. Now, for Alice to be able to put her tokens up for sale, she needs to give the marketplace approval for managing her balance:

Python
scenario.h3("Approve interaction with Aliceโ€™s balance for marketplace")
fa12.approve(spender = marketPlace.address, value = 50).run(sender = alice)

This is how the marketplace contract interacts with the FA1.2 token:

Python
scenario.h1("Marketplace")
scenario.h2("Interaction with FA1.2 token (silver ounces)")
  
scenario.h3("Register")
scenario.h4("[Error] Common user trying to register market")
params = sp.record(tokenAddress = fa12.address, tokenType = FA_1_2_TOKEN_TYPE)
marketPlace.registerMarket(params).run(sender = alice, valid = False)
scenario.h4("Register FA1.2 token")
marketPlace.registerMarket(params).run(sender = admin)
  
scenario.h3("SellAsset")
scenario.h4("Alice sells 5 ounces of silver for 1 000 mutez")
params = sp.record(tokenAddress = fa12.address, tokenId = 0, amount = sp.nat(5), price = sp.mutez(1000))
marketPlace.sellAsset(params).run(sender = alice)
lastSaleId = getLastSaleId(marketPlace)
  
scenario.h3("BuyAsset")
scenario.h4("Bob buys 5 ounces of silver for 1 000 mutez")
marketPlace.buyAsset(lastSaleId).run(sender = bob, amount = sp.mutez(1000))

Now, letโ€™s see how to implement FA2 tokens in Tezos.

Read also:
Decentralized Finance (DeFi) Solutions: Benefits, Challenges, and Best Practices to Build One

An NFT for a galleryโ€™s collection of masterpieces

In this example, we create an FA2 NFT and mint two masterpieces for Alice and Bob:

Python
scenario.h2("FA2 NFT - Gallery's collection of masterpieces")
config = FA2.FA2_config(non_fungible = True)
fa2NFT = FA2.FA2(config = config,
    metadata = sp.utils.metadata_of_url("https://example.com"),
    admin = admin.address)
scenario += fa2NFT
scenario.h3("Initial Minting")
scenario.p("The administrator mints one token-0 and one token-1.")
tokenMetadata = FA2.FA2.make_metadata(
    name = "Mona Lisa - Leonardo da Vinci",
    decimals = 0,
    symbol= "LDVMona" )
monaLisaTokenId = 0
fa2NFT.mint(address = alice.address,
                    amount = 1,
                    metadata = tokenMetadata,
                    token_id = 0).run(sender = admin)
  
tokenMetadata = FA2.FA2.make_metadata(
    name = "Guernica - Pablo Picasso",
    decimals = 0,
    symbol= "PPGuernica" )
guernicaTokenId = 1
fa2NFT.mint(address = bob.address,
                    amount = 1,
                    metadata = tokenMetadata,
                    token_id = 1).run(sender = admin)

Similarly to FA1.2, FA2 tokens can only manage a userโ€™s balance when given permission. However, this time you permit interactions with the userโ€™s balance not by giving approval but by adding a special operator. An operator doesnโ€™t require specifying a certain number of tokens, giving unlimited access to the userโ€™s balance until the operator is removed.

Hereโ€™s a snippet of adding an operator:

Python
scenario.h3("Alice gives an operator for marketPlace")
fa2NFT.update_operators([
    sp.variant("add_operator", fa2NFT.operator_param.make(
        owner = alice.address,
        operator = marketPlace.address,
        token_id = 0))
]).run(sender = alice)

And this is how our marketplace contract interacts with the FA2 NFT:

Python
scenario.h2("Interaction with FA2 NFT - Gallery's collection of masterpieces")
scenario.h3("Register token")
params = sp.record(tokenAddress = fa2NFT.address, tokenType = FA_2_TOKEN_TYPE)
marketPlace.registerMarket(params).run(sender = admin)
  
scenario.h3("SellAsset")
scenario.h4("Alice sells 'Mona Lisa' token")
params = sp.record(tokenAddress = fa2NFT.address, tokenId = monaLisaTokenId, amount = sp.nat(1), price = sp.mutez(24 * (10 ** 6)))
marketPlace.sellAsset(params).run(sender = alice) 
lastSaleId = getLastSaleId(marketPlace)
  
scenario.h3("BuyAsset")
scenario.h4("Bob buys 'Mona Lisa' token")
marketPlace.buyAsset(lastSaleId).run(sender = bob, amount = sp.mutez(24 * (10 ** 6)))

An FA2 token for an ICO platform

Now, letโ€™s create an FA2 token for a STAXE ICO platform where users can create products and seek investment for them. In this scenario, the initialization of a smart contract is similar to the scenario with gallery masterpieces, with only one difference โ€” the non_fungible flag is disabled:

Python
scenario.h2("FA2 multi-asset fungible token - STAXE ICO platform")
config = FA2.FA2_config()
fa2FungibleMultiAsset = FA2.FA2(config = config,
    metadata = sp.utils.metadata_of_url("https://staxe.io/creatives-en"),
    admin = admin.address)
scenario += fa2FungibleMultiAsset
scenario.h3("Initial minting")
scenario.p("The administrator mints invested tokens")
tokenMetadata = FA2.FA2.make_metadata(
    name = "Wake Nโ€™Wave Festival",
    decimals = 0,
    symbol= "MonarEP" )
fa2FungibleMultiAsset.mint(address = alice.address,
                    amount = 100,
                    metadata = tokenMetadata,
                    token_id = 0).run(sender = admin)
monarEPTokenId = 0
  
tokenMetadata = FA2.FA2.make_metadata(
    name = "Luna Llena - Plaza de Toros Las Ventas Madrid concert",
    decimals = 0,
    symbol= "LunaLlena" )
fa2FungibleMultiAsset.mint(address = bob.address,
                    amount = 40,
                    metadata = tokenMetadata,
                    token_id = 1).run(sender = admin)
  
scenario.p("Alice adds operator for marketPlace")
fa2FungibleMultiAsset.update_operators([
    sp.variant("add_operator", fa2FungibleMultiAsset.operator_param.make(
        owner = alice.address,
        operator = marketPlace.address,
        token_id = monarEPTokenId))
]).run(sender = alice)

In our examples above, we initialized four smart contracts: a marketplace smart contract, one contract for an FA1.2 token, and two contracts for FA2 tokens. 

Read also:
Hyperledger Fabric: Concepts, Configuration, and Deployment

Error handling and security in FA1.2 and FA2

In FA12, the error handling is relatively straightforward but effective. Specific error messages are returned to guide both developers and users:

  • notenoughbalance. This error is triggered when a user attempts to transfer more tokens than they possess. This is a crucial safeguard against unauthorized transactions.
  • unsafeallowancechange. This error occurs when there’s an attempt to change the allowance of tokens from a non-zero value to another non-zero value without first setting it to zero. This prevents potential manipulation of token allowances.

FA2 offers more comprehensive error handling, adding layers of security and robustness:

  • Batch Transfer Failures. If a batch transfer fails at any point, the entire transaction is reverted. This ensures atomicity and prevents partial transfers that could lead to inconsistent states.
  • Specific Error Messages. FA2 introduces error messages like fa2insufficientbalance and fa2tokenundefined, which provide clear indications of what went wrong. This is particularly useful for debugging and provides a better user experience.

As for real-world implications, a discussion on Tezos Stack Exchange highlighted that not fully implementing the FA standard could lead to issues in token recognition by platforms like TzKT. This underlines the importance of adhering to the standard’s error-handling mechanisms for seamless interaction with other platforms and services.

Continuing the security enhancement topic, both FA12 and FA2 have built-in error-handling mechanisms that not only make the development process easier but also enhance the overall security of the Tezos ecosystem. These mechanisms act as the first line of defense against common vulnerabilities like re-entrancy attacks or allowance manipulation. 

By providing clear and specific error messages, these standards help in identifying and preventing potential security risks, thereby contributing to a more secure and stable platform.

Conclusion

When implementing new smart contracts and tokens, developers should first learn about the mandatory requirements of the platform theyโ€™re working with. In Tezos, you can choose between two token standards โ€” FA1.2 and FA2. The first will work best for tokenizing traditional fungible assets, such as currencies and precious metals. The FA2 standard, in turn, can be used for operating with both fungible tokens and NFTs.

It’s worth noting that the Tezos community is actively discussing further improvements in error handling, as seen in issues on GitLab related to FA2. This indicates that the Tezos ecosystem is continually evolving to address the complexities and challenges of blockchain security.

At Apriorit, we have a team of passionate blockchain developers with deep knowledge of Tezos, Ethereum, Hyperledger Fabric, and other platforms. Get in touch with us to start discussing your blockchain project right away!

Have a question?

Ask our expert!

Tell us about
your project

...And our team will:

  • Process your request within 1-2 business days.
  • Get back to you with an offer based on your project's scope and requirements.
  • Set a call to discuss your future project in detail and finalize the offer.
  • Sign a contract with you to start working on your project.

Do not have any specific task for us in mind but our skills seem interesting? Get a quick Apriorit intro to better understand our team capabilities.